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

# Upsert a customer by externalId

> Creates a customer when no customer with the given externalId exists (201), or patches the existing one (200). Only the provided fields are written on update. externalId is the natural key and is required.




## OpenAPI

````yaml PUT /customers
openapi: 3.1.0
info:
  title: Protodesk API
  version: 0.1.0
  description: >
    The Protodesk Platform API: sync your customers in, attach your own business
    objects (orders, bookings, listings) to conversations, post and read
    messages across channels, and receive every workspace event via signed
    webhooks or a resumable SSE stream.


    Authenticate every request with a workspace-scoped API key: `Authorization:
    Bearer pk_live_...`. Create endpoints accept an `Idempotency-Key` header so
    retries are always safe. Lists use opaque cursor pagination (`data` /
    `hasMore` / `nextCursor`).


    Task-oriented guides: [docs.protodesk.io](https://docs.protodesk.io).
servers:
  - url: https://api.protodesk.io/v1
    description: Production
  - url: http://localhost:8080/v1
    description: Local development
security: []
tags:
  - name: Customers
    description: People or companies contacting a workspace across channels.
  - name: Conversations
    description: Customer conversations attached to channels and external business objects.
  - name: Messages
    description: Customer, agent, AI, system, and internal messages inside conversations.
  - name: Channel identities
    description: >-
      The same customer across WhatsApp, email, chat, and more — linked
      identities per channel.
  - name: Statuses & priorities
    description: The workspace's conversation statuses and priority levels.
  - name: Assignments
    description: >-
      Who a conversation has been assigned to, over time. Assign via POST
      /conversations/{conversationId}/assign.
  - name: Webhooks
    description: Webhook subscriptions for receiving workspace events.
  - name: Realtime
    description: Server-sent event stream of workspace events.
  - name: API Keys
    description: Create, list, and revoke workspace API keys.
  - name: Account
    description: The workspace and scopes behind your API key.
  - name: System
    description: Service metadata and health endpoints.
paths:
  /customers:
    put:
      tags:
        - Customers
      summary: Upsert a customer by externalId
      description: >
        Creates a customer when no customer with the given externalId exists
        (201), or patches the existing one (200). Only the provided fields are
        written on update. externalId is the natural key and is required.
      operationId: upsertCustomer
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertCustomerRequest'
      responses:
        '200':
          description: Existing customer patched.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '201':
          description: New customer created.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        default:
          $ref: '#/components/responses/Error'
      security:
        - ApiKeyAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Optional retry key for safely replaying create requests. Reusing the
        same key with a different request body returns 409.
      schema:
        type: string
        minLength: 8
        maxLength: 255
        example: 8db8e596-7c1a-4fd5-a728-4d6c99f4e66b
  schemas:
    UpsertCustomerRequest:
      type: object
      description: >
        externalId is the natural key and is required. On update, only the
        provided fields are written; omitted fields are left unchanged.
      required:
        - externalId
      properties:
        externalId:
          type: string
          description: Your stable id for this customer; the upsert key.
          example: renter_123
        name:
          type: string
          example: Maya
        email:
          type: string
          format: email
          example: maya@example.com
        phone:
          type: string
          example: '+628123456789'
        company:
          type: string
          example: Bali Villa Hub
        language:
          type: string
          example: id
        timezone:
          type: string
          example: Asia/Jakarta
        metadata:
          type: object
          additionalProperties: true
          description: >
            Replaces the full customer metadata object. Evelin reads account
            context only from the reserved tenant object and only permits id,
            name, plan, status, domain, region, createdAt, and updatedAt.
          example:
            tier: gold
    Customer:
      type: object
      description: >
        Every key is always present; optional fields are null when unset (never
        empty strings).
      required:
        - id
        - externalId
        - name
        - email
        - phone
        - company
        - language
        - timezone
        - avatarUrl
        - claimedEmail
        - emailStatus
        - lastActiveChannel
        - firstContactAt
        - lastSeenAt
        - tags
        - metadata
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          example: cus_x6q5vl75f5d53m7oet2k4r5w6a
        externalId:
          type:
            - string
            - 'null'
          example: renter_123
        name:
          type: string
          example: Maya
        email:
          type:
            - string
            - 'null'
          example: maya@example.com
        phone:
          type:
            - string
            - 'null'
          example: '+628123456789'
        company:
          type:
            - string
            - 'null'
          example: Bali Villa Hub
        language:
          type:
            - string
            - 'null'
          example: id
        timezone:
          type:
            - string
            - 'null'
          example: Asia/Jakarta
        avatarUrl:
          type:
            - string
            - 'null'
          example: https://cdn.example.com/avatars/maya.png
        claimedEmail:
          type:
            - string
            - 'null'
          description: An email the customer claimed but that is not verified.
        emailStatus:
          type:
            - string
            - 'null'
          description: >-
            Email deliverability/suppression state (e.g. active, unsubscribed,
            bounced).
        lastActiveChannel:
          type:
            - string
            - 'null'
          example: whatsapp
        firstContactAt:
          type:
            - string
            - 'null'
          format: date-time
        lastSeenAt:
          type:
            - string
            - 'null'
          format: date-time
        tags:
          type: array
          items:
            type: string
        conversationCount:
          type: integer
          description: >-
            Number of conversations for this customer. Present on list responses
            only.
        metadata:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/Error'
    Error:
      type: object
      required:
        - code
        - message
        - requestId
      properties:
        code:
          type: string
          example: route.not_found
        message:
          type: string
          example: Not found
        requestId:
          type: string
          example: req_x6q5vl75f5d53m7oet2k4r5w6a
  headers:
    RequestId:
      description: Request id for tracing this request in logs and support.
      schema:
        type: string
        example: req_x6q5vl75f5d53m7oet2k4r5w6a
    RateLimitLimit:
      description: >-
        Request budget for the route class (read or write) applied to this
        request.
      schema:
        type: integer
        example: 40
    RateLimitRemaining:
      description: Requests remaining in the current budget after this request.
      schema:
        type: integer
        example: 39
    RateLimitReset:
      description: Seconds until the budget refills to its full limit.
      schema:
        type: integer
        example: 1
    RetryAfter:
      description: Seconds to wait before retrying, sent with 429 responses.
      schema:
        type: integer
        example: 1
  responses:
    Error:
      description: >-
        Error response. All /v1 responses carry RateLimit-Limit,
        RateLimit-Remaining, and RateLimit-Reset headers. A 429
        (rate_limit.exceeded) additionally carries Retry-After. Request bodies
        exceeding the route-class limit (1KB reads, 64KB writes) return 413
        (request.body_too_large).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: Protodesk API key
      description: >
        Use a workspace API key in the Authorization header. Keys use
        pk_live_<key_id>_<secret> for production and pk_test_<key_id>_<secret>
        for test mode.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.