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

# Create or update a user

> Create or update one of **your** users, keyed by `(workspace, external_user_id)`. Sending the same `external_user_id` again updates the record; omitted fields are left untouched — so this is both create and update.

<Note>**Users are a subset of contacts.** A contact is anyone Peeve has seen. Those your product identified are `kind: "user"`; the rest are anonymous `kind: "visitor"` records Peeve mints itself. This endpoint writes users only — it requires a real `external_user_id`. To read them back, use [`GET /v1/contacts`](/api/v1/contacts/list-contacts) with `?kind=user`.</Note>

**Credential: workspace secret key (`sk_…`)** — not a user token. This runs in your backend on a signup hook or a schedule; tying a production pipeline to one employee's token would break the day they leave.

Identity fields are encrypted at rest. `plan` is a plaintext display name. `attributes` is the server-side equivalent of the browser's `setContext`.

<Warning>This endpoint predates the `/v1` envelope and keeps its original response shape for compatibility with live callers. It is the one endpoint that does not return `{ object, data, request_id }`.</Warning>



## OpenAPI

````yaml /openapi/v1.json post /v1/users
openapi: 3.1.0
info:
  title: Peeve API
  version: '2026-08-17'
  description: >-
    The `/v1` API. Server-to-server only — no CORS headers are sent, no `Origin`
    is checked, and publishable keys are rejected everywhere.


    Most endpoints take a **user token** (`pv_ut_…`), which carries its owner's
    live workspace role. `/v1/users` and `/v1/answer` take the **workspace
    secret key** (`sk_…`). The credential and the required role are stated on
    every operation — they are not uniform.
servers:
  - url: https://api.peeve.ai
security:
  - userToken: []
tags:
  - name: Sessions
    description: Agent conversations and their transcripts.
  - name: Contacts
    description: >-
      Everyone Peeve knows about: **users** your product identified, and
      anonymous **visitors**. Filter with `?kind=`.
  - name: Users
    description: Push your identified users into Peeve from your backend.
  - name: Ask
    description: Ask the agent a question from your backend and get a grounded answer.
  - name: Leads
    description: >-
      Visitors the agent qualified. Read and re-status only — capture happens
      through Peeve's own channels.
  - name: Meetings
    description: Meetings booked through the agent.
  - name: Hand-offs
    description: Escalations to a human, and their status.
  - name: Analytics
    description: Usage and satisfaction results.
  - name: Configuration
    description: Guardrails and the kill switch.
  - name: Webhooks
    description: >-
      Register and manage the endpoints Peeve delivers events to. Equivalent to
      the Connectors UI — an endpoint created here is the same row, with the
      same guarantees.
paths:
  /v1/users:
    post:
      tags:
        - Users
      summary: Create or update a user
      description: >-
        Create or update one of **your** users, keyed by `(workspace,
        external_user_id)`. Sending the same `external_user_id` again updates
        the record; omitted fields are left untouched — so this is both create
        and update.


        <Note>**Users are a subset of contacts.** A contact is anyone Peeve has
        seen. Those your product identified are `kind: "user"`; the rest are
        anonymous `kind: "visitor"` records Peeve mints itself. This endpoint
        writes users only — it requires a real `external_user_id`. To read them
        back, use [`GET /v1/contacts`](/api/v1/contacts/list-contacts) with
        `?kind=user`.</Note>


        **Credential: workspace secret key (`sk_…`)** — not a user token. This
        runs in your backend on a signup hook or a schedule; tying a production
        pipeline to one employee's token would break the day they leave.


        Identity fields are encrypted at rest. `plan` is a plaintext display
        name. `attributes` is the server-side equivalent of the browser's
        `setContext`.


        <Warning>This endpoint predates the `/v1` envelope and keeps its
        original response shape for compatibility with live callers. It is the
        one endpoint that does not return `{ object, data, request_id
        }`.</Warning>
      operationId: pushUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserPush'
            example:
              external_user_id: user_8412
              email: dana@example.com
              name: Dana Whitfield
              company: Example Corp
              plan: Growth
              created_at: '2026-02-14T09:31:00Z'
              attributes:
                seats: 12
                role: admin
      responses:
        '200':
          description: Stored. Legacy shape — no envelope.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  external_user_id:
                    type: string
                  workspace:
                    type: string
              example:
                id: 00000000-0000-4000-8000-000000000000
                external_user_id: user_8412
                workspace: example-corp
        '400':
          description: Malformed JSON, or `external_user_id` missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
              example:
                error: external_user_id is required
        '401':
          description: Not a valid secret key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
              example:
                error: a secret key (sk_*) is required
        '500':
          description: Could not store the user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
              example:
                error: could not store user
        '503':
          description: Persistence not configured.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
      security:
        - secretKey: []
components:
  schemas:
    UserPush:
      type: object
      required:
        - external_user_id
      properties:
        external_user_id:
          type: string
          description: >-
            Your stable id for this user. The identity key — it ties the record
            to its history, so it must survive key rotation.
        email:
          type: string
          format: email
          description: Encrypted at rest, indexed by a keyed hash so lookup still works.
        name:
          type: string
          description: Encrypted at rest.
        company:
          type: string
          description: Encrypted at rest.
        phone:
          type: string
          description: Encrypted at rest.
        plan:
          type: string
          maxLength: 80
          description: >-
            The user's plan in *your* product, as a display name. Plaintext; it
            does not affect your Peeve entitlements.
        created_at:
          type: string
          description: >-
            When the account was created in your product. Distinguishes a new
            signup from a returning user browsing logged out.
        vendor_token:
          type: string
          description: >-
            A token your backend issues for this user, stored encrypted so a
            Custom API connector can call your API as them. Never travels
            through the browser.
        attributes:
          type: object
          additionalProperties: true
          description: >-
            Free-form business context, merged into the user's stored
            attributes. Plaintext and non-secret — identity belongs in the
            fields above.
    LegacyError:
      type: object
      description: >-
        `/v1/users` and `/v1/answer` predate the envelope and return this flat
        shape.
      properties:
        error:
          type: string
        reason:
          type: string
  securitySchemes:
    userToken:
      type: http
      scheme: bearer
      description: >-
        A user token (`pv_ut_…`) as `Authorization: Bearer pv_ut_…`.
        `X-Peeve-Key` is accepted too. **Never a query parameter** — URLs end up
        in proxy logs and `Referer` headers.


        The token carries its owner's **live** workspace role, re-read on every
        call.
    secretKey:
      type: http
      scheme: bearer
      description: >-
        The workspace secret key (`sk_…`) as `Authorization: Bearer sk_…`.
        `X-Peeve-Key` is accepted too.

````