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

# Create or update a contact

> Send a contact from your CRM using an ID that stays stable across synchronizations. Requires `contacts:write`. With `externalId`, this request updates the matching contact in the workspace or creates one if no match exists. Without it, a new logical request creates a new contact; phone-number equality does not perform an upsert.

Supply an allowed current workspace member as `ownerId`, an E.164 number, and any contact details. The full write shape applies on updates: omitted defaulted fields reset. A non-null timezone requires `provided` or `phone_estimate`; null requires `unknown`. This request does not add list membership.

Persist an `Idempotency-Key` and reuse it with the same request after a timeout. Matching retries return the original status/body. `403 invalid_owner` requires a permitted member; `409 external_id_conflict` means the external ID belongs outside your owner scope. Correct `400` validation errors before sending a new operation.

See [contact writes](/guides/contact-lists#create-or-update-a-contact).



## OpenAPI

````yaml /api/openapi.json post /contacts
openapi: 3.1.0
info:
  title: PowerDialer API v2
  version: 2.0.0
  description: >-
    Connect your CRM, sales workspace or reporting tools to PowerDialer using
    scoped API keys. Manage contacts and calling lists, read call evidence and
    analytics, and receive signed events. All example records and IDs are
    synthetic; replace them with values from your workspace. Shared-key v1 is
    retired. API availability was verified on 2026-10-08; see /availability for
    historical data coverage. Legacy outbound webhooks use a separate contract.
servers:
  - url: https://api.migration.powerdialer.ai/api/public/v2
    description: Production API v2; availability verified on 2026-10-08.
security:
  - ApiKey: []
tags:
  - name: Authentication
  - name: Contacts
  - name: Calling lists
  - name: Imports
  - name: Calls
  - name: Synchronization
  - name: Analytics
  - name: Webhooks
  - name: Webhook deliveries
  - name: Incoming webhook payloads
paths:
  /contacts:
    post:
      tags:
        - Contacts
      summary: Create or update a contact
      description: >-
        Send a contact from your CRM using an ID that stays stable across
        synchronizations. Requires `contacts:write`. With `externalId`, this
        request updates the matching contact in the workspace or creates one if
        no match exists. Without it, a new logical request creates a new
        contact; phone-number equality does not perform an upsert.


        Supply an allowed current workspace member as `ownerId`, an E.164
        number, and any contact details. The full write shape applies on
        updates: omitted defaulted fields reset. A non-null timezone requires
        `provided` or `phone_estimate`; null requires `unknown`. This request
        does not add list membership.


        Persist an `Idempotency-Key` and reuse it with the same request after a
        timeout. Matching retries return the original status/body. `403
        invalid_owner` requires a permitted member; `409 external_id_conflict`
        means the external ID belongs outside your owner scope. Correct `400`
        validation errors before sending a new operation.


        See [contact writes](/guides/contact-lists#create-or-update-a-contact).
      operationId: postContacts
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            pattern: ^[A-Za-z0-9._:-]{1,200}$
          description: >-
            Persist one key for this logical operation before sending it. Reuse
            the same value only with the same method, path and body after a
            timeout. A changed request returns 409 idempotency_conflict; a
            matching retry returns the original response.
          example: 9d6b623c-c132-4d2f-b8cf-7c65d4760dbe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactWrite'
            example:
              ownerId: user_example
              externalId: crm-contact-123
              name: Alex Morgan
              phoneNumber: '+12125550100'
              additionalNumbers: []
              metadata:
                source: website
              timeZone: America/New_York
              timeZoneSource: provided
      responses:
        '200':
          description: >-
            The existing contact was updated, or a successful update receipt was
            replayed.
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            Idempotency-Replayed:
              schema:
                type: string
                enum:
                  - 'true'
                  - 'false'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
              example:
                ownerId: user_example
                externalId: crm-contact-123
                name: Alex Morgan
                phoneNumber: '+12125550100'
                additionalNumbers: []
                metadata:
                  source: website
                  timeZone: America/New_York
                  timeZoneSource: provided
                timeZone: America/New_York
                timeZoneSource: provided
                id: 123
                createdAt: '2026-10-08T12:00:00.000Z'
                updatedAt: '2026-10-08T12:00:00.000Z'
        '201':
          description: >-
            A contact was created, or the original creation receipt was
            replayed.
          headers:
            X-Request-Id:
              schema:
                type: string
                format: uuid
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            Idempotency-Replayed:
              schema:
                type: string
                enum:
                  - 'true'
                  - 'false'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Contact'
              example:
                ownerId: user_example
                externalId: crm-contact-123
                name: Alex Morgan
                phoneNumber: '+12125550100'
                additionalNumbers: []
                metadata:
                  source: website
                  timeZone: America/New_York
                  timeZoneSource: provided
                timeZone: America/New_York
                timeZoneSource: provided
                id: 123
                createdAt: '2026-10-08T12:00:00.000Z'
                updatedAt: '2026-10-08T12:00:00.000Z'
        default:
          description: >-
            Error. 400 invalid request; 401 invalid credential; 403 insufficient
            access; 404 not found; 409 state/idempotency conflict; 413 size
            limit; 422 verification failure; 429 rate limit (Retry-After
            seconds); 503 temporary service failure.
          headers:
            Retry-After:
              schema:
                type: integer
            X-Request-Id:
              schema:
                type: string
                format: uuid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: invalid_api_key
                  message: API key is invalid, expired or revoked
                  requestId: 00000000-0000-4000-8000-000000000004
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >-
            # Replace example resource IDs and ownerId with values from your
            workspace.

            : "${POWERDIALER_API_KEY:?Set POWERDIALER_API_KEY to your scoped API
            key}"

            # Set this once per operation; reuse it with the same request after
            a timeout.

            : "${POWERDIALER_REQUEST_ID:?Set POWERDIALER_REQUEST_ID to a new
            UUID for this operation}"

            curl --request POST \
              --url 'https://api.migration.powerdialer.ai/api/public/v2/contacts' \
              --header "Authorization: Bearer $POWERDIALER_API_KEY" \
              --header "Idempotency-Key: $POWERDIALER_REQUEST_ID" \
              --header 'Content-Type: application/json' \
              --data '{
              "ownerId": "user_example",
              "externalId": "crm-contact-123",
              "name": "Alex Morgan",
              "phoneNumber": "+12125550100",
              "additionalNumbers": [],
              "metadata": {
                "source": "website"
              },
              "timeZone": "America/New_York",
              "timeZoneSource": "provided"
            }'
components:
  schemas:
    ContactWrite:
      type: object
      properties:
        externalId:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            Stable contact ID from your CRM or source system, unique within this
            workspace. Reuse it on POST to update an existing contact.
        ownerId:
          type: string
          minLength: 1
          maxLength: 100
          description: >-
            ID of an active PowerDialer workspace member allowed by this key.
            Personal keys must use their own ownerId from GET /me.
        name:
          type: string
          maxLength: 500
          default: ''
          description: >-
            Contact display name. Omitting this field on a write resets it to an
            empty string.
        phoneNumber:
          type: string
          pattern: ^\+[1-9]\d{6,14}$
          description: Primary number in E.164 format, including + and country code.
        additionalNumbers:
          type: array
          items:
            type: string
            pattern: ^\+[1-9]\d{6,14}$
          maxItems: 10
          default: []
          description: >-
            Up to 10 additional E.164 numbers. Replaces the stored array;
            omitted means empty.
        metadata:
          type: object
          additionalProperties: true
          default: {}
          description: >-
            Your JSON object. Its JSON.stringify output must fit within 16,000
            UTF-16 code units. Replaces existing metadata. Top-level timezone
            fields overwrite the matching metadata keys.
        timeZone:
          anyOf:
            - type: string
              maxLength: 100
            - type: 'null'
          default: null
          description: >-
            IANA timezone such as America/New_York. A non-null value requires
            timeZoneSource provided or phone_estimate; null or omission requires
            unknown. The API does not infer a timezone.
        timeZoneSource:
          type: string
          enum:
            - provided
            - phone_estimate
            - unknown
          default: unknown
          description: >-
            provided for a known timezone, phone_estimate for an estimate made
            by your integration, or unknown when timeZone is null.
      required:
        - ownerId
        - phoneNumber
      additionalProperties: false
      examples:
        - ownerId: user_example
          externalId: crm-contact-123
          name: Alex Morgan
          phoneNumber: '+12125550100'
          additionalNumbers: []
          metadata:
            source: website
          timeZone: America/New_York
          timeZoneSource: provided
    Contact:
      type: object
      properties:
        id:
          type: integer
          description: PowerDialer contact ID. Use this integer in contact URLs.
        name:
          type: string
          description: Stored contact name.
        phoneNumber:
          type: string
          description: Stored primary phone number. V2 writes require E.164.
        additionalNumbers:
          type: array
          items:
            type: string
          description: Stored additional phone numbers.
        metadata:
          description: >-
            Stored JSON metadata. v2 writes use objects; legacy rows may contain
            other JSON values.
        ownerId:
          anyOf:
            - type: string
            - type: 'null'
          description: Attributed PowerDialer member ID.
        externalId:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            Your source-system identifier, or null when no identifier was
            assigned.
        createdAt:
          type: string
          format: date-time
          description: Time the contact was created, in UTC.
        updatedAt:
          type: string
          format: date-time
          description: Time the contact was last updated, in UTC.
        timeZone:
          description: >-
            Stored timezone metadata. V2 writes produce an IANA string or null;
            legacy metadata may contain another JSON value.
        timeZoneSource:
          description: >-
            Stored timezone-source metadata. V2 writes use provided,
            phone_estimate, or unknown; legacy values may differ.
      required:
        - id
        - name
        - phoneNumber
        - additionalNumbers
        - metadata
        - ownerId
        - externalId
        - createdAt
        - updatedAt
        - timeZone
        - timeZoneSource
      examples:
        - ownerId: user_example
          externalId: crm-contact-123
          name: Alex Morgan
          phoneNumber: '+12125550100'
          additionalNumbers: []
          metadata:
            source: website
            timeZone: America/New_York
            timeZoneSource: provided
          timeZone: America/New_York
          timeZoneSource: provided
          id: 123
          createdAt: '2026-10-08T12:00:00.000Z'
          updatedAt: '2026-10-08T12:00:00.000Z'
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
              description: Human-readable explanation.
            requestId:
              type: string
              format: uuid
              description: Request identifier to include when investigating a failure.
            details:
              type: array
              items:
                type: object
                properties:
                  path:
                    type: string
                  message:
                    type: string
                required:
                  - path
                  - message
              description: Validation errors, when available.
          required:
            - code
            - message
            - requestId
      required:
        - error
      examples:
        - error:
            code: insufficient_scope
            message: Requires contacts:read
            requestId: 00000000-0000-4000-8000-000000000004
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: pd_<live|test>_<credential UUID>.<random secret>
      description: >-
        Issued in Developers. Workspace/owner/scopes come from credential, never
        acting-user headers. API keys cannot manage API credentials.

````

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