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

# Crear o actualizar un contacto

> Escribe un contacto identificado por su **teléfono**: si ya existe uno con ese número se actualiza, si no se crea. Una clave ausente deja el valor guardado como está, así que una solicitud parcial es una actualización parcial.

Los atributos personalizados van al mismo nivel que los campos fijos, identificados por su nombre en Revem. Un nombre sin definición responde `422` con la lista en `unknownAttributes`; un valor que no calza con el tipo declarado también responde `422`.

Un contacto que estaba en la papelera, o que pertenece al simulador, sigue siendo dueño de su número: escribirle responde `409` y se resuelve desde la app.



## OpenAPI

````yaml /openapi.json post /contacts/public/v1
openapi: 3.1.0
info:
  title: Revem API
  version: 1.0.0
  description: >-
    La API pública de Revem para integraciones de servidor a servidor:
    contactos, tickets y el canal de webchat. Se autentica con la API key de tu
    organización.
  contact:
    name: Soporte Revem
    email: soporte@revem.ai
    url: https://revem.ai
servers:
  - url: https://api.revem.ai
    description: Producción
security:
  - apiKey: []
tags:
  - name: Contactos
    description: >-
      Sincroniza tu base de clientes con Revem. Los contactos se identifican por
      teléfono al crearlos y por id después.
  - name: Tickets
    description: Lee y actualiza los tickets de tu embudo desde tu sistema de gestión.
  - name: Webchat
    description: >-
      Conecta el chat de tu sitio con Revem: registra lo que escribe el
      visitante y lee las respuestas del equipo o del agente.
paths:
  /contacts/public/v1:
    post:
      tags:
        - Contactos
      summary: Crear o actualizar un contacto
      description: >-
        Escribe un contacto identificado por su **teléfono**: si ya existe uno
        con ese número se actualiza, si no se crea. Una clave ausente deja el
        valor guardado como está, así que una solicitud parcial es una
        actualización parcial.


        Los atributos personalizados van al mismo nivel que los campos fijos,
        identificados por su nombre en Revem. Un nombre sin definición responde
        `422` con la lista en `unknownAttributes`; un valor que no calza con el
        tipo declarado también responde `422`.


        Un contacto que estaba en la papelera, o que pertenece al simulador,
        sigue siendo dueño de su número: escribirle responde `409` y se resuelve
        desde la app.
      operationId: upsertContact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactUpsertBody'
            example:
              phone_number: '+56912345678'
              first_name: Camila
              last_name: Rojas
              email: camila@ejemplo.cl
              programa: Premium
      responses:
        '200':
          description: Contacto creado o actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactUpsertResult'
              example:
                ok: true
                id: 3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90
                created: true
                warnings: []
        '400':
          description: El cuerpo no cumple el esquema (por ejemplo, falta `phone_number`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsError'
              example:
                ok: false
                error: >-
                  body/phone_number Invalid input: expected string, received
                  undefined
        '401':
          description: Sin credencial, o la API key no existe o fue revocada.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              example:
                error: Unauthorized
                message: Invalid API key
        '403':
          description: La credencial no puede escribir este contacto.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsError'
        '409':
          description: El teléfono pertenece a un contacto en la papelera o del simulador.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsError'
              example:
                ok: false
                error: Conflict
                message: >-
                  El teléfono pertenece a un contacto en la papelera; restáuralo
                  o elimínalo desde Contactos
        '422':
          description: >-
            Un atributo no existe, es ambiguo, es de sólo lectura o su valor no
            calza con el tipo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactsError'
              example:
                ok: false
                error: Unprocessable Entity
                message: 'Atributos sin definición: "embcodigo"'
                unknownAttributes:
                  - embcodigo
        '429':
          description: >-
            La key superó su límite de solicitudes por minuto. El header
            `retry-after` indica cuánto esperar.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
              example:
                ok: false
                error: Too many requests. Retry in 1 minute.
          headers:
            retry-after:
              description: Segundos que esperar antes de reintentar.
              schema:
                type: integer
            x-ratelimit-limit:
              description: Solicitudes permitidas por minuto.
              schema:
                type: integer
            x-ratelimit-remaining:
              description: Solicitudes que quedan en la ventana actual.
              schema:
                type: integer
            x-ratelimit-reset:
              description: Segundos hasta que la ventana se reinicia.
              schema:
                type: integer
components:
  schemas:
    ContactUpsertBody:
      type: object
      properties:
        phone_number:
          type: string
          minLength: 1
          description: >-
            Teléfono del contacto. Se recomienda formato E.164 (`+56912345678`);
            otras grafías se normalizan al guardar. Es la identidad: si ya
            existe un contacto con este número, se actualiza; si no, se crea.
        first_name:
          type: string
          description: Nombre. Si se omite, el valor guardado no cambia.
        last_name:
          type: string
          description: Apellido. Si se omite, el valor guardado no cambia.
        email:
          type: string
          format: email
          description: >-
            Si otro contacto del mismo tenant ya tiene este email, se conserva
            el resto de la solicitud y se informa en `warnings`.
      required:
        - phone_number
      additionalProperties: true
      description: >-
        Cualquier otra clave del cuerpo se interpreta como un **atributo
        personalizado** del contacto, identificado por su nombre tal como está
        definido en Revem. Un nombre que no existe responde `422`; se aceptan
        hasta 100 atributos por solicitud.
    ContactUpsertResult:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        id:
          type: string
          format: uuid
          description: >-
            Identificador del contacto creado o actualizado. Guárdalo: es la
            clave estable para `PATCH /{id}` y para webchat.
        created:
          type: boolean
          description: >-
            `true` si la solicitud creó el contacto; `false` si el teléfono ya
            existía y se actualizó.
        warnings:
          type: array
          items:
            type: string
          description: >-
            Avisos sobre una solicitud que SÍ escribió. Hoy el único caso es un
            email que no se guardó porque pertenece a otro contacto.
      required:
        - ok
        - id
        - created
        - warnings
      additionalProperties: false
    ContactsError:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
          description: >-
            Etiqueta del código HTTP (`Bad Request`, `Conflict`, `Unprocessable
            Entity`…).
        message:
          type: string
          description: Explicación legible del problema.
        unknownAttributes:
          type: array
          items:
            type: string
          description: >-
            Presente en el `422` por atributos desconocidos: los nombres que el
            tenant no tiene definidos.
      required:
        - error
      additionalProperties: false
    AuthError:
      type: object
      properties:
        ok:
          type: boolean
          const: false
          description: Presente sólo en el `403`.
        error:
          type: string
        message:
          type: string
      required:
        - error
        - message
    RateLimitError:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
          description: Incluye cuánto esperar antes de reintentar.
      required:
        - ok
        - error
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        La API key de tu organización, con la forma `rvm_<prefijo>.<secreto>`,
        enviada como `Authorization: Bearer rvm_…`. Revem la emite y sólo se
        muestra una vez; si la pierdes se reemplaza, no se recupera.

````