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

# Upsert a partner user

> Idempotently provisions or updates a consumer from the partner backend. Keyed by `(partner_id, externalUserId)`. If the user was previously anonymised via `DELETE`, calling this again revives the row and clears `anonymizedAt`. PII fields that are omitted are left unchanged; passing an explicit `null` clears them.



## OpenAPI

````yaml /openapi.json post /integrations/users
openapi: 3.1.0
info:
  title: Voucher Platform — Partner Integration API
  version: 1.0.0
  description: >-
    Server-to-server and consumer endpoints used by partner apps to provision
    users, embed the voucher webview, and receive lifecycle webhooks. See the
    [partner integration guide](/docs) for the full narrative including JWT
    setup, HMAC signing, and a worked end-to-end example.
servers:
  - url: http://localhost:4000
    description: Local development
security: []
tags:
  - name: Integrations
    description: >-
      Server-to-server endpoints called from the partner backend. Authenticated
      with an HMAC signature.
  - name: Consumer
    description: >-
      Called from inside the partner-issued webview (or partner-native UI) on
      behalf of an end user. Authenticated with a partner-signed JWT.
paths:
  /integrations/users:
    post:
      tags:
        - Integrations
      summary: Upsert a partner user
      description: >-
        Idempotently provisions or updates a consumer from the partner backend.
        Keyed by `(partner_id, externalUserId)`. If the user was previously
        anonymised via `DELETE`, calling this again revives the row and clears
        `anonymizedAt`. PII fields that are omitted are left unchanged; passing
        an explicit `null` clears them.
      parameters:
        - schema:
            type: string
          in: header
          name: x-partner-slug
          required: true
          description: Your partner slug, assigned at onboarding (e.g. `acme`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - externalUserId
              additionalProperties: false
              properties:
                externalUserId:
                  type: string
                  minLength: 1
                  maxLength: 255
                  description: >-
                    Your stable identifier for this user (typically your DB
                    primary key). Treated as opaque — we never decode it. Used
                    as the JWT `sub` claim and in every outbound webhook payload
                    to link your records to ours.
                email:
                  type:
                    - string
                    - 'null'
                  format: email
                  maxLength: 320
                  description: User email. Pass `null` to clear; omit to leave unchanged.
                displayName:
                  type:
                    - string
                    - 'null'
                  maxLength: 200
                  description: >-
                    Shown in our admin views and (absent a JWT override) in the
                    webview.
                phone:
                  type:
                    - string
                    - 'null'
                  maxLength: 40
                  description: >-
                    E.164 recommended but not enforced. Used only for support
                    lookups.
                countryCode:
                  type:
                    - string
                    - 'null'
                  minLength: 2
                  maxLength: 2
                  description: ISO 3166-1 alpha-2. Auto-uppercased server-side.
                locale:
                  type:
                    - string
                    - 'null'
                  maxLength: 16
                  description: BCP-47 locale tag (e.g. `en-SG`). Currently informational.
      responses:
        '200':
          description: User already existed; row updated in place.
          content:
            application/json:
              schema:
                description: User already existed; row updated in place.
                type: object
                additionalProperties: true
                properties:
                  userId:
                    type: string
                    format: uuid
                    description: >-
                      Our internal user UUID. Stable across requests; safe to
                      persist.
                  created:
                    type: boolean
                    enum:
                      - false
        '201':
          description: New user created.
          content:
            application/json:
              schema:
                description: New user created.
                type: object
                additionalProperties: true
                properties:
                  userId:
                    type: string
                    format: uuid
                  created:
                    type: boolean
                    enum:
                      - true
        '400':
          description: Validation failed (e.g. missing externalUserId).
          content:
            application/json:
              schema:
                description: Validation failed (e.g. missing externalUserId).
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    example: validation_failed
        '401':
          description: >-
            Authentication failed. `error` is one of `missing_partner_slug`,
            `missing_signature`, `unknown_partner`, `invalid_signature_format`,
            `signature_expired`, `invalid_signature`.
          content:
            application/json:
              schema:
                description: >-
                  Authentication failed. `error` is one of
                  `missing_partner_slug`, `missing_signature`,
                  `unknown_partner`, `invalid_signature_format`,
                  `signature_expired`, `invalid_signature`.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    example: invalid_signature
        '403':
          description: Partner exists but is disabled.
          content:
            application/json:
              schema:
                description: Partner exists but is disabled.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    example: partner_disabled
        '500':
          description: Unexpected server-side failure during upsert.
          content:
            application/json:
              schema:
                description: Unexpected server-side failure during upsert.
                type: object
                additionalProperties: true
                properties:
                  error:
                    type: string
                    example: upsert_failed
      security:
        - PartnerHmac: []
components:
  securitySchemes:
    PartnerHmac:
      type: apiKey
      in: header
      name: x-signature
      description: >-
        Stripe-style HMAC signature over `${timestamp}.${rawBody}` using your
        partner secret. Format: `t=<unix-seconds>,v1=<hex-hmac-sha256>`. Also
        requires the `x-partner-slug` header to identify your account.

````