> ## 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 producto

> Escribe un producto identificado por su **SKU**: si ya existe uno con ese SKU 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.

Guarda el `id` que devuelve: es la identidad estable, y es lo que permite cambiarle el SKU después con `PATCH /products/public/v1/{id}`.

Un producto del simulador sigue siendo dueño de su SKU: escribirle responde `409` y se resuelve desde la app. Un atributo marcado como obligatorio hay que enviarlo al crear; cuando el SKU ya existe la solicitud es una actualización y sólo se revisan los atributos que nombra.



## OpenAPI

````yaml /openapi.json post /products/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, organizaciones, 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: Organizaciones
    description: >-
      Sincroniza empresas por identificador tributario, con atributos
      personalizados por nombre.
  - 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:
  /products/public/v1:
    post:
      tags:
        - Productos
      summary: Crear o actualizar un producto
      description: >-
        Escribe un producto identificado por su **SKU**: si ya existe uno con
        ese SKU 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.


        Guarda el `id` que devuelve: es la identidad estable, y es lo que
        permite cambiarle el SKU después con `PATCH /products/public/v1/{id}`.


        Un producto del simulador sigue siendo dueño de su SKU: escribirle
        responde `409` y se resuelve desde la app. Un atributo marcado como
        obligatorio hay que enviarlo al crear; cuando el SKU ya existe la
        solicitud es una actualización y sólo se revisan los atributos que
        nombra.
      operationId: upsertProduct
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpsertBody'
            example:
              sku: CONT-20ST
              name: Contenedor 20 pies
              price: 1250000
              currency: CLP
              Estado: Disponible
      responses:
        '200':
          description: Producto creado o actualizado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductUpsertResult'
              example:
                ok: true
                id: 3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90
                created: true
                warnings: []
        '400':
          description: El cuerpo no cumple el esquema (falta `sku`, `name` o `price`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductsError'
              example:
                ok: false
                error: 'body/price Invalid input: expected number, 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 usar esta ruta. Una API key sólo alcanza los
            endpoints documentados aquí; cualquier otra ruta de la API responde
            este código.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              example:
                ok: false
                error: Forbidden
                message: This endpoint is not available to API keys
        '409':
          description: >-
            El SKU lo usa un producto del simulador, o una integración
            administra la URL que se intenta escribir.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductsError'
              example:
                ok: false
                error: Conflict
                message: >-
                  El SKU "CONT-20ST" lo usa un producto del simulador;
                  resuélvelo en Productos
        '422':
          description: >-
            Un atributo no existe, es ambiguo, es de sólo lectura, falta uno
            obligatorio o su valor no calza con el tipo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductsError'
              example:
                ok: false
                error: Unprocessable Entity
                message: >-
                  Atributos de producto sin definición: Terminal. Créalos en
                  Productos antes de enviarlos
                unknownAttributes:
                  - Terminal
        '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:
    ProductUpsertBody:
      type: object
      properties:
        sku:
          type: string
          minLength: 1
          maxLength: 255
          description: >-
            Identificador del producto en tu sistema. Único dentro del tenant;
            es la identidad con la que el upsert resuelve.
        name:
          type: string
          minLength: 1
          maxLength: 500
          description: Nombre del producto, hasta 500 caracteres.
        description:
          type: string
          maxLength: 5000
          description: >-
            Descripción, hasta 5000 caracteres. Una cadena vacía no borra la
            descripción guardada.
        price:
          type: number
          minimum: 0
          maximum: 9999999999.99
          description: >-
            Precio, hasta dos decimales. Un tercer decimal responde 400 en vez
            de truncarse.
        currency:
          type: string
          pattern: ^[A-Za-z]{3}$
          description: Código de moneda de 3 letras, por ejemplo CLP.
        billing_frequency:
          type: string
          enum:
            - UNIQUE
            - WEEKLY
            - MONTHLY
            - QUARTERLY
            - SEMI_ANNUAL
            - ANNUAL
          description: >-
            UNIQUE, WEEKLY, MONTHLY, QUARTERLY, SEMI_ANNUAL o ANNUAL. Al crear,
            si se omite, queda en UNIQUE.
        billing_model:
          type: string
          enum:
            - UNIQUE
            - RECURRENT
          description: UNIQUE para un cobro único o RECURRENT para uno recurrente.
        unit_of_measurement:
          type: string
          enum:
            - UNIT
            - HOUR
            - DAY
            - MONTH
            - KG
            - G
            - LITER
            - METER
            - M2
          description: UNIT, HOUR, DAY, MONTH, KG, G, LITER, METER o M2.
        maximum_percentage_discount:
          type: integer
          minimum: 0
          maximum: 100
          description: Descuento máximo permitido, entero de 0 a 100.
        tax:
          type: integer
          minimum: 0
          maximum: 100
          description: Impuesto aplicable, entero de 0 a 100.
        active:
          type: boolean
          description: >-
            Si el producto está disponible. Esta API no borra productos: dar de
            baja es enviar active: false.
        media_url:
          type: string
          maxLength: 2000
          description: >-
            Enlace http o https a la imagen. Responde 409 si la administra la
            integración que importó el producto.
        external_url:
          type: string
          maxLength: 2000
          description: >-
            Enlace http o https donde se ve o se compra. Responde 409 si la
            administra la integración que importó el producto.
      required:
        - sku
        - name
        - price
      additionalProperties: true
      description: >-
        sku, name y price son obligatorios: este cuerpo puede crear el producto
        y son las tres columnas sin valor por defecto. Los atributos
        personalizados van al mismo nivel que los campos fijos, identificados
        por su nombre en Revem (sin distinguir mayúsculas). Tienen que estar
        definidos para productos: un nombre sin definición responde 422 con la
        lista en unknownAttributes, y un valor que no calza con el tipo
        declarado —o una opción que no está en un select— también responde 422.
        Un null quita el atributo. Hasta 100 atributos por solicitud.
    ProductUpsertResult:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        id:
          type: string
          format: uuid
          description: 'ID del producto en Revem. Guárdalo: es estable aunque cambie el SKU.'
        created:
          type: boolean
          description: true si se creó el producto, false si se actualizó uno existente.
        warnings:
          type: array
          items:
            type: string
          description: Avisos que no impidieron la escritura. Hoy siempre vacío.
      required:
        - ok
        - id
        - created
        - warnings
      additionalProperties: false
      example:
        ok: true
        id: 3f2b6c1e-8d4a-4f0b-9c2e-7a1d5e6f8b90
        created: true
        warnings: []
    ProductsError:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
          description: Etiqueta del error.
        message:
          type: string
          description: Explicación legible del problema.
        unknownAttributes:
          type: array
          items:
            type: string
          description: >-
            Presente en el 422 por atributos: los nombres desconocidos, de sólo
            lectura u obligatorios que faltan.
        missingAttributes:
          type: array
          items:
            type: string
      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.

````