{
  "openapi": "3.1.0",
  "info": {
    "title": "Loybox API",
    "version": "1.0.0",
    "summary": "Puntos, premios, niveles y canjes del programa de fidelidad.",
    "description": "API REST de Loybox. Todo va y vuelve en JSON, los campos están en\n`snake_case` y las fechas en ISO 8601 con zona (`2026-03-14T18:30:00Z`).\n\nHay **dos credenciales distintas y no intercambiables**:\n\n- La **API key del comercio** (`commerceApiKey`) opera sobre todos los\n  clientes. Vive sólo en tu servidor. Cubre Consumos, Clientes y\n  Beneficios.\n- El **token del usuario final** (`userAccessToken`) sólo ve los datos de\n  ese usuario y puede vivir en el browser. Cubre Mi cuenta, y se obtiene\n  con el login por código de Autenticación.\n\nLos endpoints de Autenticación, Mi cuenta y Público requieren además el\nheader `X-Commerce-Id`, que no es secreto: delimita la respuesta a tu\nprograma, no autoriza.\n\nLas respuestas no llevan sobre: el objeto viene en la raíz y los listados\nson un array directo. Los campos opcionales vienen presentes y en `null`.",
    "contact": {
      "name": "Loybox",
      "email": "hola@loybox.com.ar",
      "url": "https://docs.loybox.com.ar"
    }
  },
  "externalDocs": {
    "description": "Referencia completa",
    "url": "https://docs.loybox.com.ar/api-reference"
  },
  "servers": [
    {
      "url": "https://loybox-public-api-752998171300.southamerica-west1.run.app",
      "description": "Producción"
    }
  ],
  "tags": [
    {
      "name": "Consumos",
      "description": "Registrar compras para que sumen puntos.",
      "externalDocs": {
        "url": "https://docs.loybox.com.ar/api-reference/consumos"
      }
    },
    {
      "name": "Clientes",
      "description": "Consultar clientes del comercio y sus beneficios.",
      "externalDocs": {
        "url": "https://docs.loybox.com.ar/api-reference/clientes"
      }
    },
    {
      "name": "Beneficios",
      "description": "Catálogo, consulta de un código de canje y canje.",
      "externalDocs": {
        "url": "https://docs.loybox.com.ar/api-reference/beneficios"
      }
    },
    {
      "name": "Autenticación",
      "description": "Login del usuario final con código por email.",
      "externalDocs": {
        "url": "https://docs.loybox.com.ar/api-reference/autenticacion"
      }
    },
    {
      "name": "Mi cuenta",
      "description": "Puntos, beneficios, historial y nivel del usuario final.",
      "externalDocs": {
        "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta"
      }
    },
    {
      "name": "Público",
      "description": "Marca y catálogo, sin token.",
      "externalDocs": {
        "url": "https://docs.loybox.com.ar/api-reference/publico"
      }
    }
  ],
  "paths": {
    "/v1/consumptions/client-code": {
      "post": {
        "operationId": "createConsumptionByClientCode",
        "tags": [
          "Consumos"
        ],
        "summary": "Crear consumo por código",
        "description": "Registra una compra usando el código del cliente. Es la llamada que suma los puntos. Si el código no existe devuelve 404 y no registra nada. La respuesta no dice cuántos puntos sumó: para eso, consultar el cliente.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/consumos/crear-por-codigo"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_code",
                  "amount"
                ],
                "properties": {
                  "client_code": {
                    "type": "integer",
                    "description": "Código del cliente que hizo la compra."
                  },
                  "amount": {
                    "type": "integer",
                    "description": "Monto de la compra, en unidades enteras de la moneda del comercio (sin decimales)."
                  },
                  "counter_id": {
                    "type": "integer",
                    "description": "Id de la caja donde se hizo la compra."
                  },
                  "id_suc": {
                    "type": "integer",
                    "description": "Id de la sucursal."
                  }
                }
              },
              "example": {
                "client_code": 12345,
                "amount": 1750,
                "counter_id": 2,
                "id_suc": 1
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Consumo creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "description": "No se pudo crear el consumo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "El cliente existe pero no es elegible para tener consumos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El cliente no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta un campo del cuerpo o tiene el tipo equivocado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/consumptions/email": {
      "post": {
        "operationId": "createConsumptionByEmail",
        "tags": [
          "Consumos"
        ],
        "summary": "Crear consumo por email",
        "description": "Registra una compra usando el email del cliente. Si el email no tiene cuenta en Loybox el consumo se crea igual y al cliente le llega una invitación: los puntos quedan esperándolo.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/consumos/crear-por-email"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_email",
                  "amount"
                ],
                "properties": {
                  "client_email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email del cliente que hizo la compra."
                  },
                  "amount": {
                    "type": "integer",
                    "description": "Monto de la compra, en unidades enteras de la moneda del comercio."
                  }
                }
              },
              "example": {
                "client_email": "ana@example.com",
                "amount": 1750
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Consumo creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "description": "No se pudo crear el consumo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "El cliente existe pero no es elegible para tener consumos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El cliente no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta un campo del cuerpo, o el email no tiene formato válido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/clients/list": {
      "get": {
        "operationId": "listClients",
        "tags": [
          "Clientes"
        ],
        "summary": "Listar clientes",
        "description": "Listado de clientes del comercio, paginado por límite y desplazamiento. Para recorrerlo todo, sumar `limit` al `offset` hasta llegar a `total`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/clientes/listar"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Cuántos clientes traer por página.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Desde qué posición arrancar.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La página pedida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientPage"
                }
              }
            }
          },
          "401": {
            "description": "La API key es inválida: no tiene un comercio asociado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`limit` fuera del rango 1–100, u `offset` negativo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/clients/{client_code}": {
      "get": {
        "operationId": "getClient",
        "tags": [
          "Clientes"
        ],
        "summary": "Obtener un cliente",
        "description": "Busca un cliente por su código. El campo `points` es el saldo disponible: puntos ganados vigentes menos canjeados.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/clientes/obtener"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "client_code",
            "in": "path",
            "required": true,
            "description": "El código del cliente.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "404": {
            "description": "El cliente no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "El código no tiene un formato válido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/clients/{client_code}/available-benefits": {
      "get": {
        "operationId": "listClientAvailableBenefits",
        "tags": [
          "Clientes"
        ],
        "summary": "Beneficios que el cliente puede comprar",
        "description": "Los beneficios que se le muestran al cliente para comprar con sus puntos. Para saber cuáles le alcanzan, comparar `cost` con los `points` del cliente.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/clientes/beneficios-disponibles"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "client_code",
            "in": "path",
            "required": true,
            "description": "El código del cliente.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El catálogo disponible.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Benefit"
                  }
                }
              }
            }
          },
          "404": {
            "description": "El cliente no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "El código no tiene un formato válido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/clients/{client_code}/benefits": {
      "get": {
        "operationId": "listClientBenefits",
        "tags": [
          "Clientes"
        ],
        "summary": "Beneficios comprados por el cliente",
        "description": "Los beneficios que el cliente ya compró con sus puntos. Sin el parámetro `used` trae sólo los canjeables (sin usar y sin vencer). El `client_benefit_code` de cada item es el código de canje.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/clientes/beneficios-comprados"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "client_code",
            "in": "path",
            "required": true,
            "description": "El código del cliente.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "used",
            "in": "query",
            "description": "Sin especificar trae sólo los canjeables. `true` trae los ya canjeados y `false` los no canjeados, en ambos casos sin filtrar por vencimiento.",
            "schema": {
              "type": [
                "boolean",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Los beneficios del cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ClientBenefit"
                  }
                }
              }
            }
          },
          "401": {
            "description": "La API key es inválida: no tiene un comercio asociado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El cliente no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "El código no tiene un formato válido, o `used` no es booleano.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/benefits/list": {
      "get": {
        "operationId": "listBenefits",
        "tags": [
          "Beneficios"
        ],
        "summary": "Listar beneficios",
        "description": "El catálogo completo del comercio, sin filtrar por vigencia ni por cliente. Incluye las recompensas especiales: filtrar por `benefit_type: \"normal\"` para quedarse con lo comprable con puntos.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/beneficios/listar"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "El catálogo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Benefit"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/benefits/{benefit_id}": {
      "get": {
        "operationId": "getBenefit",
        "tags": [
          "Beneficios"
        ],
        "summary": "Obtener un beneficio",
        "description": "Un beneficio del catálogo por su `benefit_id`. Este id identifica el beneficio del comercio, no el de un cliente concreto: para eso está el `client_benefit_code`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/beneficios/obtener"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "benefit_id",
            "in": "path",
            "required": true,
            "description": "El id del beneficio.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El beneficio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Benefit"
                }
              }
            }
          },
          "401": {
            "description": "La API key es inválida: no tiene un comercio asociado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El beneficio no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "El id no tiene un formato válido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/benefits/preview/{client_benefit_code}": {
      "get": {
        "operationId": "previewClientBenefit",
        "tags": [
          "Beneficios"
        ],
        "summary": "Consultar un código de canje",
        "description": "Qué premio es un código, antes de canjearlo. No canja nada y se puede repetir. Para una integración nueva conviene la v2, que trae `title`, `value` y `product` en la raíz.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/beneficios/consultar-codigo"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "client_benefit_code",
            "in": "path",
            "required": true,
            "description": "El código de canje que tiene el cliente.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El beneficio del código.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Benefit"
                }
              }
            }
          },
          "400": {
            "description": "El beneficio ya fue usado: no aplicar ningún descuento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "La API key es inválida: no tiene un comercio asociado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El beneficio no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "El código no tiene un formato válido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v2/benefits/preview/{client_benefit_code}": {
      "get": {
        "operationId": "previewClientBenefitV2",
        "tags": [
          "Beneficios"
        ],
        "summary": "Consultar un código de canje (v2)",
        "description": "Lo mismo que la v1, con `title`, `value` y `product` en la raíz del objeto: para aplicar el descuento ya no hace falta entrar a `prize`. El canje no tiene v2: se sigue usando `POST /v1/benefits/redeem`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/beneficios/consultar-codigo-v2"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "client_benefit_code",
            "in": "path",
            "required": true,
            "description": "El código de canje que tiene el cliente.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El beneficio del código.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BenefitV2"
                }
              }
            }
          },
          "400": {
            "description": "El beneficio ya fue usado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "La API key es inválida: no tiene un comercio asociado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El beneficio no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "El código no tiene un formato válido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/benefits/redeem": {
      "post": {
        "operationId": "redeemBenefit",
        "tags": [
          "Beneficios"
        ],
        "summary": "Canjear un beneficio",
        "description": "Marca el código como usado. Es definitivo: llamarlo recién cuando la venta esté cerrada y el descuento aplicado. Un 400 significa que el código ya se usó.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/beneficios/canjear"
        },
        "security": [
          {
            "commerceApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_benefit_code"
                ],
                "properties": {
                  "client_benefit_code": {
                    "type": "integer",
                    "description": "El código de canje que tiene el cliente."
                  }
                }
              },
              "example": {
                "client_benefit_code": 887766
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Beneficio canjeado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "400": {
            "description": "El beneficio ya fue usado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El beneficio no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el campo, o no es un entero.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/otp/request": {
      "post": {
        "operationId": "requestOtp",
        "tags": [
          "Autenticación"
        ],
        "summary": "Pedir un código",
        "description": "Manda al usuario un código de 6 dígitos por email. Responde 200 incluso si el email no tiene cuenta, a propósito: no sirve para saber si un usuario existe. El código vence a los 10 minutos, admite 5 intentos, y pedir uno nuevo invalida el anterior.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/autenticacion/pedir-codigo"
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email del usuario."
                  }
                }
              },
              "example": {
                "email": "ana@example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Código enviado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`, o el email es inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/otp/verify": {
      "post": {
        "operationId": "verifyOtp",
        "tags": [
          "Autenticación"
        ],
        "summary": "Verificar el código",
        "description": "Valida el código y devuelve los tokens de sesión. Si el email no tenía cuenta, la crea; en ambos casos deja al usuario adherido al programa del comercio de `X-Commerce-Id`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/autenticacion/verificar-codigo"
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "otp"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "El mismo email con el que se pidió el código."
                  },
                  "otp": {
                    "type": "string",
                    "minLength": 6,
                    "maxLength": 6,
                    "description": "Código de 6 dígitos recibido por email."
                  }
                }
              },
              "example": {
                "email": "ana@example.com",
                "otp": "418302"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La sesión del usuario.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "description": "El código es inválido o venció.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`, o el cuerpo es inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/refresh": {
      "post": {
        "operationId": "refreshToken",
        "tags": [
          "Autenticación"
        ],
        "summary": "Renovar el token",
        "description": "Un `access` nuevo a partir del `refresh`, sin pedirle otro código al usuario. No devuelve un `refresh` nuevo: el que ya tenías sigue siendo el válido. Un 401 acá significa que la sesión terminó y hay que pedir otro código.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/autenticacion/renovar-token"
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "refresh"
                ],
                "properties": {
                  "refresh": {
                    "type": "string",
                    "description": "El token de refresco."
                  }
                }
              },
              "example": {
                "refresh": "eyJhbGciOiJIUzI1NiIs..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "El token nuevo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RefreshedSession"
                }
              }
            }
          },
          "401": {
            "description": "El token de refresco es inválido o venció.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`, o falta `refresh`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Mi cuenta",
        "description": "Todo lo del primer render: datos del usuario, puntos, adhesión, marca del comercio y puntos por vencer. No hace falta pedir los datos del comercio aparte. `points_expiration` no viene en `null` cuando los puntos no vencen: viene con `mode: \"none\"`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/obtener"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El estado del usuario en el programa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/benefits/available": {
      "get": {
        "operationId": "listMyAvailableBenefits",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Beneficios que puedo comprar",
        "description": "El catálogo vigente. La API no filtra por saldo: comparar el `cost` de cada beneficio con los `points` de `GET /v1/me` y decidir en la UI qué es alcanzable.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/beneficios-disponibles"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El catálogo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Benefit"
                  }
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/benefits/exchange": {
      "post": {
        "operationId": "exchangeBenefit",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Comprar un beneficio",
        "description": "Cambia puntos por un beneficio y devuelve el `client_benefit_code`, que es el código de canje y también el código de cupón en una tienda online. Es la única llamada de Mi cuenta que mueve puntos: usar `Idempotency-Key` para que un reintento no cobre dos veces.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/comprar-beneficio"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por compra iniciada (un UUID alcanza), repetida en todos los reintentos de esa compra.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "benefit_id"
                ],
                "properties": {
                  "benefit_id": {
                    "type": "string",
                    "description": "Id del beneficio a comprar."
                  }
                }
              },
              "example": {
                "benefit_id": "b_9f2a"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "El beneficio comprado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientBenefit"
                }
              }
            }
          },
          "400": {
            "description": "El beneficio no existe, no está vigente, o no le alcanzan los puntos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`, o falta `benefit_id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/benefits": {
      "get": {
        "operationId": "listMyBenefits",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Mis beneficios",
        "description": "Los que el usuario ya compró y todavía puede canjear, cada uno con su `client_benefit_code`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/mis-beneficios"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Los beneficios del usuario.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ClientBenefit"
                  }
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/activity": {
      "get": {
        "operationId": "listMyActivity",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Mi historial",
        "description": "Consumos, compras de beneficios, canjes y recompensas. Paginado por cursor: pasar el `next_cursor` de la respuesta anterior en `?cursor=`. Los campos cargados dependen del `type`, así que conviene un `switch`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/historial"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Cursor de la página a traer. Sin especificar, trae la primera.",
            "schema": {
              "type": [
                "string",
                "null"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La página de movimientos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivityPage"
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/level": {
      "get": {
        "operationId": "getMyLevel",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Mi nivel",
        "description": "Nivel actual, multiplicador y progreso al siguiente. El progreso se mide contra el acumulado histórico (`total_earned_points` o `total_spent_amount` según el `threshold_type`), no contra el saldo: canjear no baja de nivel. El 404 es el caso normal de un cliente sin nivel: corresponde esconder la sección, no mostrar un error.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/nivel"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El nivel del usuario.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Level"
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El usuario todavía no tiene nivel, o el comercio no usa niveles.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/rewards": {
      "get": {
        "operationId": "listMyRewards",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Recompensas del programa",
        "description": "Las recompensas que el comercio entrega solas (bienvenida, cumpleaños, cliente del mes). Es lo que el programa ofrece, no lo que el usuario ya recibió: eso está en el historial.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/recompensas"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Las recompensas configuradas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AutoReward"
                  }
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El comercio no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/subscription": {
      "post": {
        "operationId": "subscribe",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Adherirme al programa",
        "description": "Adhiere al usuario y devuelve la recompensa de bienvenida en `welcome_benefit` o `welcome_points` (son excluyentes). Verificar el código ya adhiere, así que esto sirve sobre todo para volver después de una baja. No lleva cuerpo.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/adherirme"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Adhesión hecha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Subscription"
                }
              }
            }
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El comercio no existe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "unsubscribe",
        "tags": [
          "Mi cuenta"
        ],
        "summary": "Darme de baja",
        "description": "Da de baja al usuario del programa. Los puntos acumulados no se pierden: si vuelve a adherirse, siguen estando. No lleva cuerpo y la respuesta no tiene forma definida: alcanza el código de estado.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/mi-cuenta/darme-de-baja"
        },
        "security": [
          {
            "userAccessToken": []
          }
        ],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Baja hecha."
          },
          "401": {
            "description": "El token falta, venció o no corresponde a un usuario final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El comercio no existe, o el usuario no estaba adherido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/public/commerce": {
      "get": {
        "operationId": "getPublicCommerce",
        "tags": [
          "Público"
        ],
        "summary": "Datos del comercio",
        "description": "Nombre, logo, color de marca y rubro, sin token. Para el header del programa antes del login: con sesión, los mismos datos vienen dentro de `GET /v1/me`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/publico/comercio"
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El comercio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Commerce"
                }
              }
            }
          },
          "400": {
            "description": "No se pudo obtener el comercio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/public/benefits": {
      "get": {
        "operationId": "listPublicBenefits",
        "tags": [
          "Público"
        ],
        "summary": "Catálogo de beneficios",
        "description": "Los beneficios vigentes del comercio, sin token: la vidriera para un visitante sin sesión. Con sesión conviene `GET /v1/me/benefits/available`.",
        "externalDocs": {
          "description": "Documentación",
          "url": "https://docs.loybox.com.ar/api-reference/publico/beneficios"
        },
        "security": [],
        "parameters": [
          {
            "name": "X-Commerce-Id",
            "in": "header",
            "required": true,
            "description": "Id del comercio que integra la API. Limita la respuesta a ese programa.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El catálogo público.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Benefit"
                  }
                }
              }
            }
          },
          "400": {
            "description": "No se pudieron obtener los beneficios del comercio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Falta el header `X-Commerce-Id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "commerceApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "La API key del comercio, en `Authorization: Bearer {api-key}`. Da acceso a los datos de todos los clientes del comercio: vive sólo en tu servidor, nunca en el frontend."
      },
      "userAccessToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "El token de acceso del usuario final, en `Authorization: Bearer {access}`. Lo devuelve `POST /v1/auth/otp/verify` y sólo ve los datos de ese usuario, así que puede vivir en el browser. Va siempre junto al header `X-Commerce-Id`."
      }
    },
    "schemas": {
      "Message": {
        "type": "object",
        "description": "Respuesta con un solo mensaje para el desarrollador.",
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "examples": [
          {
            "message": "Consumption created successfully"
          }
        ]
      },
      "Error": {
        "type": "object",
        "description": "Cuerpo de error. El `message` es para el desarrollador y puede cambiar sin aviso: en el código conviene mirar el código de estado.",
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "examples": [
          {
            "message": "Client not found"
          }
        ]
      },
      "ValidationError": {
        "type": "object",
        "description": "Error de validación (422). La causa más frecuente no es el cuerpo: es el header `X-Commerce-Id` que falta.",
        "properties": {
          "detail": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "loc",
                "msg",
                "type"
              ],
              "properties": {
                "loc": {
                  "type": "array",
                  "items": {
                    "type": [
                      "string",
                      "integer"
                    ]
                  },
                  "description": "Dónde está el problema: el primer elemento es la parte del pedido (`body`, `query`, `header`, `path`) y el resto el camino al campo."
                },
                "msg": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                }
              }
            }
          }
        },
        "examples": [
          {
            "detail": [
              {
                "loc": [
                  "body",
                  "amount"
                ],
                "msg": "Input should be a valid integer",
                "type": "int_parsing"
              }
            ]
          }
        ]
      },
      "Client": {
        "type": "object",
        "required": [
          "code",
          "username",
          "email"
        ],
        "properties": {
          "code": {
            "type": "integer",
            "description": "El código del cliente."
          },
          "username": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "points": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Saldo disponible: puntos ganados vigentes menos canjeados."
          }
        },
        "examples": [
          {
            "code": 12345,
            "username": "Ana Pérez",
            "email": "ana@example.com",
            "points": 340
          }
        ]
      },
      "ClientPage": {
        "type": "object",
        "required": [
          "items",
          "total",
          "limit",
          "offset"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Client"
            }
          },
          "total": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Total de clientes del comercio, sin paginar."
          },
          "limit": {
            "type": [
              "integer",
              "null"
            ]
          },
          "offset": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Benefit": {
        "type": "object",
        "description": "Un beneficio del catálogo del comercio. Su `id` es el `benefit_id`, que sirve para consultar y para comprar, pero no para canjear.",
        "required": [
          "id",
          "type",
          "description",
          "cost",
          "expiration"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "El `benefit_id`."
          },
          "type": {
            "type": "string",
            "enum": [
              "percentage_discount",
              "absolute_discount",
              "free_product"
            ]
          },
          "description": {
            "type": "string"
          },
          "cost": {
            "type": "number",
            "description": "Cuántos puntos cuesta."
          },
          "expiration": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Hasta cuándo está vigente. `null` si no vence."
          },
          "benefit_type": {
            "type": "string",
            "enum": [
              "normal",
              "welcome",
              "birthday",
              "monthly_top",
              "level"
            ],
            "default": "normal",
            "description": "`normal` para los del catálogo; el resto son recompensas especiales que el comercio entrega solas."
          },
          "color": {
            "type": [
              "string",
              "null"
            ],
            "description": "Color de marca del comercio, en hexadecimal."
          },
          "buy_limit": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Canjes permitidos en total. `0` es sin límite."
          },
          "prize": {
            "$ref": "#/components/schemas/Prize"
          },
          "tiendanube_coupon": {
            "$ref": "#/components/schemas/TiendanubeCoupon"
          }
        },
        "examples": [
          {
            "id": "b_9f2a",
            "type": "percentage_discount",
            "description": "20% de descuento en toda la tienda",
            "cost": 250,
            "expiration": "2026-12-31T23:59:59Z",
            "benefit_type": "normal",
            "color": "#f1a10d",
            "buy_limit": 0,
            "prize": null,
            "tiendanube_coupon": null
          }
        ]
      },
      "BenefitV2": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Benefit"
          },
          {
            "type": "object",
            "required": [
              "title"
            ],
            "description": "Los tres campos que la v2 sube a la raíz para no tener que entrar a `prize`.",
            "properties": {
              "title": {
                "type": "string"
              },
              "value": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "El porcentaje si es `percentage_discount`, el monto si es `absolute_discount`."
              },
              "product": {
                "$ref": "#/components/schemas/Product"
              }
            }
          }
        ]
      },
      "ClientBenefit": {
        "type": "object",
        "description": "Un beneficio ya comprado por un cliente. Su `client_benefit_code` es el que se consulta y se canjea, y en una tienda online es el código de cupón.",
        "required": [
          "client_benefit_code",
          "issue_date",
          "benefit"
        ],
        "properties": {
          "client_benefit_code": {
            "type": "integer",
            "description": "El código de canje."
          },
          "issue_date": {
            "type": "string",
            "format": "date-time"
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Hasta cuándo puede canjearlo. `null` si no vence."
          },
          "used": {
            "type": "boolean",
            "default": false
          },
          "benefit": {
            "$ref": "#/components/schemas/Benefit"
          }
        }
      },
      "Prize": {
        "type": [
          "object",
          "null"
        ],
        "description": "El detalle de lo que gana el cliente.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "percentage_discount",
              "absolute_discount",
              "free_product"
            ]
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "value": {
            "type": [
              "number",
              "null"
            ],
            "description": "El porcentaje o el monto, según el `type`."
          },
          "product": {
            "$ref": "#/components/schemas/Product"
          },
          "expiration": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "Product": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": [
              "integer",
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id del producto en el sistema del comercio."
          }
        }
      },
      "Commerce": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "El mismo id que va en el header `X-Commerce-Id`."
          },
          "name": {
            "type": "string"
          },
          "logo": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "color": {
            "type": [
              "string",
              "null"
            ]
          },
          "category_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "username": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "points": {
            "type": "integer",
            "default": 0,
            "description": "Puntos del usuario en este comercio."
          },
          "subscribed": {
            "type": "boolean",
            "default": false,
            "description": "Si está adherido al programa. En `false`, mostrar el llamado a adherirse en lugar del catálogo."
          },
          "commerce": {
            "$ref": "#/components/schemas/Commerce"
          },
          "points_expiration": {
            "$ref": "#/components/schemas/PointsExpiration"
          }
        }
      },
      "PointsExpiration": {
        "type": [
          "object",
          "null"
        ],
        "description": "Puntos por vencer. Cuando el comercio no hace vencer los puntos no viene en `null`: viene con `mode: \"none\"` y `points: 0`.",
        "properties": {
          "points": {
            "type": "integer",
            "default": 0
          },
          "expiration_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "days_left": {
            "type": [
              "integer",
              "null"
            ]
          },
          "months_left": {
            "type": [
              "integer",
              "null"
            ]
          },
          "mode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "none",
              "rolling",
              "accumulated",
              null
            ],
            "description": "Cómo vencen los puntos en este comercio. `none` es que no vencen."
          }
        }
      },
      "Level": {
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Posición del nivel en la escalera."
          },
          "points_multiplier": {
            "type": [
              "number",
              "null"
            ]
          },
          "icon_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "reached_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiration_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_expired": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "next_level": {
            "$ref": "#/components/schemas/NextLevel"
          },
          "total_earned_points": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Acumulado histórico de puntos ganados."
          },
          "total_spent_amount": {
            "type": [
              "number",
              "null"
            ]
          },
          "total_consumptions_count": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "NextLevel": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ]
          },
          "threshold_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Con qué se mide el umbral: por puntos acumulados o por monto gastado."
          },
          "threshold": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "Activity": {
        "type": "object",
        "description": "Un movimiento del historial. Los campos que vienen cargados dependen del `type`.",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "consumption",
              "benefit_exchange",
              "benefit_usage",
              "points_special_reward"
            ]
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "points": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Puntos que sumó o restó. Negativo en las compras."
          },
          "benefit": {
            "$ref": "#/components/schemas/Benefit"
          },
          "amount": {
            "type": [
              "number",
              "null"
            ],
            "description": "Monto de la compra, en los `consumption`."
          },
          "event": {
            "type": [
              "string",
              "null"
            ],
            "description": "El evento que disparó la recompensa, en los `points_special_reward`."
          },
          "additional_note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ActivityPage": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Activity"
            }
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Se pasa como `?cursor=` para la página siguiente. `null` cuando no hay más."
          },
          "previous_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "AutoReward": {
        "type": "object",
        "description": "Una recompensa que el comercio entrega sola. El `reward_type` dice dónde mirar: el otro campo viene en `null`.",
        "required": [
          "event",
          "reward_type"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "welcome",
              "birthday",
              "monthly_top"
            ]
          },
          "reward_type": {
            "type": "string",
            "enum": [
              "benefit",
              "points"
            ]
          },
          "benefit": {
            "$ref": "#/components/schemas/Benefit"
          },
          "points": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Subscription": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string"
          },
          "welcome_benefit": {
            "$ref": "#/components/schemas/Benefit"
          },
          "welcome_points": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Excluyente con `welcome_benefit`: viene uno o el otro, o los dos en `null`."
          }
        }
      },
      "Session": {
        "type": "object",
        "required": [
          "access",
          "refresh",
          "expires_in",
          "user_id"
        ],
        "properties": {
          "access": {
            "type": "string",
            "description": "Token de acceso, para `Authorization: Bearer {access}`."
          },
          "refresh": {
            "type": "string",
            "description": "Token de refresco, para renovar el acceso."
          },
          "expires_in": {
            "type": "integer",
            "description": "Segundos de validez del token de acceso."
          },
          "user_id": {
            "type": "integer"
          },
          "username": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RefreshedSession": {
        "type": "object",
        "required": [
          "access"
        ],
        "description": "No trae un `refresh` nuevo: el que ya tenías sigue siendo el válido.",
        "properties": {
          "access": {
            "type": "string"
          },
          "expires_in": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "TiendanubeCoupon": {
        "type": [
          "object",
          "null"
        ],
        "description": "El cupón, cuando el beneficio se aplica en una tienda de Tiendanube.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "percentage",
              "absolute",
              "shipping"
            ]
          },
          "value": {
            "type": "number"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id de la categoría de Tiendanube a la que aplica."
          },
          "category_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "end_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "product": {
            "$ref": "#/components/schemas/TiendanubeProduct"
          }
        }
      },
      "TiendanubeProduct": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "tiendanube_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "available": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "published": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "brand": {
            "type": [
              "string",
              "null"
            ]
          },
          "categories": {
            "type": "array",
            "default": [],
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": [
                    "integer",
                    "string"
                  ]
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  }
}