> Versión Markdown de https://pricevault.es/api-docs

Referencia de la API

# Documentación de la API

Integra el catálogo de tarifas de luz y gas de PriceVault en tu CRM, ERP o comparador.
Consulta ofertas, compáralas y encuentra la más eficiente — el equilibrio entre el mejor precio para tu cliente y la mejor comisión para ti.

Base URL https://pricevault.es/v1

## Introducción

La API de PriceVault es REST sobre HTTPS, con peticiones y respuestas en JSON. Todos los endpoints
programáticos viven bajo el prefijo `/v1`. Esta página documenta los endpoints destacados;
para la referencia exhaustiva de todos los endpoints y esquemas dispones de la especificación OpenAPI:

- [Swagger UI](https://pricevault.es/docs) — interactivo, prueba endpoints desde el navegador.
- [ReDoc](https://pricevault.es/redoc) — referencia completa, limpia y legible.

## Autenticación

Cada petición se autentica con tu API key en la cabecera `X-API-Key`. Genera y gestiona tus
claves desde el dashboard. Trátalas como un secreto: nunca las publiques en el frontend ni en repositorios.

cURL

```
curl "https://pricevault.es/v1/offers" \
  -H "X-API-Key: TU_API_KEY"
```

- API key inválida o inactiva → `401 Unauthorized`.
- Cuenta desactivada → `403 Forbidden`.

## Scopes

Cada API key puede limitarse a un conjunto de *scopes* (permisos). Si llamas a un endpoint sin el
scope requerido, la respuesta es `403` con el scope que falta.

Una API key **sin scopes asignados** tiene acceso completo. Para integraciones, crea claves con el mínimo de scopes necesarios.

| Scope | Permite |
| --- | --- |
| `offers:read` | Consultar ofertas, comparar y recomendaciones |
| `offers:write` | Crear, editar y eliminar ofertas |
| `offers:export` | Exportar ofertas a CSV |
| `commissions:read` | Ver comisiones (ofertas, ranking, recomendación, plantillas y calculadora) |
| `indexed_prices:read` | Consultar precios indexados históricos |
| `companies:read` | Consultar comercializadoras |
| `companies:write` | Crear, editar y eliminar comercializadoras |
| `clients:read` | Consultar asesorías (clientes) |
| `clients:write` | Crear, editar y eliminar asesorías (clientes) |
| `catalog:read` | Consultar catálogo de ofertas compartidas |
| `catalog:write` | Crear, editar y eliminar entradas del catálogo |

### Claves vinculadas a una asesoría

Una API key puede estar vinculada a una sola asesoría. Cuando lo está, el parámetro `client_id`
de las consultas se fuerza a esa asesoría; pasar otro distinto devuelve `403`. Es el detalle
que más confusión genera al integrar.

## Convenciones

### Paginación

Los listados aceptan `limit` (1–200, por defecto 50) y `offset`. La respuesta
envuelve los datos: `{ "total", "limit", "offset", "results": [...] }`.

### Estado de los registros

Por defecto solo se devuelven registros activos; usa `include_inactive=true` para incluir los
desactivados. Las ofertas de una comercializadora desactivada (propia o del catálogo) quedan fuera de los
listados, comparativas, rankings y estadísticas aunque la oferta en sí esté activa; con
`include_inactive=true` vuelven a aparecer. El borrado (`DELETE`) sobre ofertas y
asesorías es lógico (`is_active = false`); sobre comercializadoras es **físico** e
irreversible (se llevan sus ofertas): para ocultarlas sin perder nada, desactívalas.

### Códigos de error

| Código | Significado |
| --- | --- |
| `400` | Petición o formato de archivo inválido. |
| `401` | Falta la API key o es inválida/inactiva. |
| `403` | Sin el scope requerido o sin acceso a esa asesoría. |
| `404` | El recurso no existe. |
| `409` | Conflicto (nombre duplicado, estado incompatible). |
| `413` | Archivo mayor de 10 MB. |
| `422` | Validación de negocio (territorio, tipo de cliente, etc.). |
| `429` | Límite de peticiones superado. |

### Límites de uso

Rate limit de **uso razonable (anti-abuso), no una cuota**: las llamadas
no se facturan. Escala con el plan en dos niveles — un techo por cuenta y, para las
keys atadas a una asesoría concreta (`client_id`), un sub-límite por
cliente. Con una key de cuenta (sin `client_id`) solo aplica el techo. Al
superarlo, la API responde `429`.

| Plan | Techo por cuenta (req/min) | Por cliente (req/min) |
| --- | --- | --- |
| Gratis / Trial / Starter | 60 | 30 |
| Pro | 150 | 40 |
| Business | 400 | 60 |

## Servidor MCP

Además de REST, PriceVault expone un servidor **MCP (Model Context Protocol)** para que un
agente de IA use la API como herramienta nativa, sin que escribas la integración. Es la misma API
`/v1` de esta página: cada herramienta re-entra por el mismo control de autenticación, scopes
y límites de uso.

Endpoint MCP https://pricevault.es/mcp/

- Transporte **Streamable HTTP**; autenticación con la cabecera `X-API-Key`, igual que en REST.
- **Solo lectura.** Solo se exponen endpoints de consulta y cálculo. La key dedicada de
  MCP lleva `offers:read` y `catalog:read`, y además `commissions:read`
  solo si la creas para uso interno: una key para un bot que habla con clientes no ve comisiones.
- Incluido en todos los planes: las llamadas no se facturan y comparten el rate limit de la sección anterior.

El proveedor de IA que conectes al MCP lo eliges tú, así que es **subencargado tuyo**, no de PriceVault: necesitas tu propia base legal y acuerdo de tratamiento (DPA) con él. Ver [cláusula 16](https://pricevault.es/legal/dpa#api-mcp).

[Guía completa del servidor MCP](https://pricevault.es/mcp) — herramientas disponibles y cómo configurar Claude,
n8n u otro cliente.

## Listar tarifas

**GET** `/v1/offers` — Scope: offers:read

El corazón de la API: lista las tarifas de tu organización con filtros ricos por suministro, tarifa de acceso, territorio, comunidad, tipo de cliente y comercializadora.

### Parámetros de consulta

| Parámetro | Tipo | Descripción |
| --- | --- | --- |
| supply_type | `LUZ` · `GAS` | Tipo de suministro (por defecto `LUZ`). |
| tariff_type | string | Tarifa de acceso. Ej. `2.0TD`, `3.0TD`, `RL.1`. |
| company_id | int | Filtrar por comercializadora. |
| company_name | string | Búsqueda parcial por nombre (sin distinción de mayúsculas). |
| territory | enum | `peninsula`, `canarias`, `baleares`, `ceuta_melilla`. |
| community | enum | Comunidad autónoma (incluye ofertas sin restricción geográfica). |
| placas_solares | bool | Filtrar ofertas con placas solares. |
| client_type | `dni` · `cif` · `cdad` | Particulares (DNI), empresas (CIF) o comunidades de propietarios. |
| client_id | int | Filtrar por asesoría (forzado si la key está vinculada). |
| include_inactive | bool | Incluir ofertas inactivas. |
| limit / offset | int | Paginación. |

cURL

```
curl "https://pricevault.es/v1/offers?supply_type=LUZ&tariff_type=2.0TD&territory=peninsula&limit=20" \
  -H "X-API-Key: $PV_KEY"
```

Respuesta 200

```
{
  "total": 134,
  "limit": 20,
  "offset": 0,
  "results": [
    {
      "id": 412,
      "offer_name": "Tarifa Hogar 2.0TD",
      "supply_type": "LUZ",
      "tariff_type": "2.0TD",
      "is_indexed": false,
      "company": {
        "id": 12, "name": "Energía X", "logo_url": "https://...", "is_active": true,
        "client": { "id": 3, "name": "Asesoría Norte", "image_url": null }
      },
      "e1": 0.142, "e2": 0.108, "e3": 0.085,
      "p1": 0.084, "p2": 0.012,
      "commission": 25.0, "commission_type": "fixed",
      "acepta_dni": true, "acepta_cif": true, "acepta_cdad_propietarios": true,
      "permanencia_meses": 12,
      "is_active": true,
      "created_at": "2026-04-02T10:11:00Z",
      "updated_at": "2026-05-30T08:00:00Z"
    }
  ]
}
```

Cada oferta incluye solo los campos de su suministro. Una oferta `GAS` lleva `gas_energy` y `gas_fixed`, y no incluye `e1`–`e6`, `p1`–`p6`, `fee_power`, `placas_solares`, `precio_excedentes`, `ssaa_max_eur_mwh` ni `commission_power_tiers`. Una oferta `LUZ` no incluye `gas_energy` ni `gas_fixed`. Lo mismo aplica a las filas de `indexed_prices` y a todos los endpoints que devuelven ofertas, incluido el catálogo. Trata esas claves como opcionales.

Las comisiones requieren commissions:read. Con una key de scopes limitados sin ese permiso, `commission`, `commission_type`, `commission_tiers` y `commission_power_tiers` (este último no existe en ofertas `GAS`) llegan a `null` en este endpoint, en `GET /v1/offers/{id}` y en `GET /v1/catalog/offers`, y las columnas de comisión del CSV de `GET /v1/offers/export` salen vacías. Úsala así en bots o asistentes que hablan con tus clientes.

## Oferta más eficiente

**POST** `/v1/offers/efficient` — Scope: offers:read

El motor comparador. Dado un perfil de consumo y potencia, rankea las ofertas combinando coste y comisión con una nota de eficiencia. El peso `w` desplaza el equilibrio entre ahorro para el cliente y comisión para ti.

Sin el scope commissions:read el ranking es solo por precio: `w` vale `1`, enviar un `w` menor devuelve `403`, y `commission`, `nota_comision` y `mas_comision_id` llegan a `null`.

El coste (`cost`) es **sin impuestos** (IVA, IGIC o IPSI, impuesto eléctrico e impuesto de hidrocarburos) y sin alquiler de contador: sirve para comparar ofertas, no es el importe de la factura. Esos conceptos son iguales para todas las ofertas de un mismo cliente, así que no cambian el ranking.

Cada oferta incluye sus precios por periodo y `updated_at`. En las indexadas, `indexada` indica el precio aplicado y de dónde sale: `"modo": "media"` (media de `meses_historico` meses, de `desde` a `hasta`) o `"modo": "mes"` (el mes pedido en modo mensual). Las ofertas que no se pueden calcular salen en `excluded` con su `motivo`: una indexada sin histórico (o sin el mes pedido), o una oferta sin precio en un periodo donde el cliente consume, que de otro modo saldría artificialmente barata.

### Cuerpo (JSON)

| Campo | Tipo | Descripción |
| --- | --- | --- |
| supply_type obligatorio | `LUZ` · `GAS` | Tipo de suministro. |
| energia | objeto `e1..e6` | Consumo por periodo en kWh durante el periodo de cálculo (por defecto un año) (LUZ). |
| potencia | objeto `p1..p6` | Potencia contratada por periodo, kW (LUZ). |
| consumo_gas | float | Consumo de gas en kWh durante el periodo de cálculo. **Obligatorio** (>0) si `supply_type=GAS`. |
| dias / meses | int | Periodo de cálculo: `dias` para la potencia, `meses` para el término fijo de gas y los servicios. Si envías solo uno, el otro se deriva (`dias=30` → `meses≈0.99`); sin ninguno, un año (365 / 12). La respuesta devuelve los dos valores usados. |
| fee_energia / fee_potencia | float 1–30 | Fee en €/MWh (`fee_potencia` solo LUZ). |
| w | float 0–1 | Peso ahorro↔comisión (`1` = solo ahorro, `0` = solo comisión; por defecto `0.5`; `1` sin `commissions:read`). |
| tariff_type, territory, limit | — | Filtros del universo a rankear. |
| company_name | string | Filtra por nombre de comercializadora (parcial, sin distinguir mayúsculas), en las ofertas propias y en las del catálogo. |
| company_id | int | Id de una comercializadora propia; deja fuera el catálogo. Para filtrar en general, usa `company_name`. |
| ssaa_included | bool | Filtra por SSAA en precio: `true` = incluidos, `false` = aparte. Omítelo para rankear todas. |
| sin_permanencia | bool | Filtra por permanencia: `true` = solo ofertas sin permanencia, `false` = solo con permanencia. Omítelo para rankear todas. No cambia el coste; cada oferta devuelve `permanencia_meses`. |
| ssaa_eur_mwh | float 0–1000 | Solo LUZ. SSAA (€/MWh) a aplicar en todos los meses en lugar de los datos mensuales de REE. Las ofertas con SSAA aparte, o incluidos solo hasta un tope (`ssaa_max_eur_mwh`), suman el recargo a `cost.ssaa`. Sin `ssaa_eur_mwh` y sin datos de REE no se suma recargo y se avisa en `warnings`. |
| client_type | enum | `dni` (particulares), `cif` (empresas) o `cdad` (comunidades de propietarios). |

cURL

```
curl -X POST "https://pricevault.es/v1/offers/efficient" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "supply_type": "LUZ",
    "energia": { "e1": 1200, "e2": 800, "e3": 600 },
    "potencia": { "p1": 4.6, "p2": 4.6 },
    "fee_energia": 5,
    "fee_potencia": 3,
    "w": 0.5,
    "tariff_type": "2.0TD",
    "limit": 10
  }'
```

cURL · GAS

```
curl -X POST "https://pricevault.es/v1/offers/efficient" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "supply_type": "GAS",
    "consumo_gas": 12000,
    "fee_energia": 3,
    "w": 0.5,
    "tariff_type": "RL.1",
    "limit": 10
  }'
```

Respuesta 200

```
{
  "w": 0.5,
  "dias": 365,
  "meses": 12,
  "total": 48,
  "excluded_count": 1,
  "excluded": [
    { "offer_id": 501, "offer_name": "Indexada Plus", "company_name": "Energía Y",
      "is_catalog": false, "motivo": "indexada sin histórico de precios" }
  ],
  "mas_barata_id": 412,
  "mas_comision_id": 533,
  "mas_eficiente_id": 488,
  "warnings": [],
  "ssaa": { "fuente": "ree", "eur_mwh_medio": 20.9, "desde": "2025-10-01", "hasta": "2026-09-01" },
  "results": [
    {
      "offer": {
        "id": 488, "offer_name": "Tarifa Óptima",
        "company": { "id": 12, "name": "Energía X", "logo_url": "...", "is_active": true },
        "e1": 0.142, "e2": 0.118, "e3": 0.096, "p1": 0.0821, "p2": 0.0041,
        "services_cost": null, "permanencia_meses": 12, "updated_at": "2026-10-01T09:12:00"
      },
      "indexada": null,
      "cost": { "energia": 312.4, "potencia": 145.0, "fijo": 0.0, "excedentes": 0.0, "servicios": 0.0, "ssaa": 0.0, "total": 457.4 },
      "commission": { "energia": 18.0, "potencia": 6.0, "total": 24.0 },
      "nota": 8.7, "nota_precio": 9.1, "nota_comision": 8.3,
      "is_optimal": true
    }
  ]
}
```

## Mayor comisión por consumo y potencia

**GET** `/v1/offers/best-commission` — Scope: offers:read commissions:read

Ordena las ofertas por la comisión esperada según el consumo anual (`consumption_mwh`) y la potencia contratada (`power_kw`, solo LUZ). Cada resultado incluye la comisión por consumo y por potencia con el tramo aplicado. Para recomendar al cliente final la oferta más barata, usa `POST /v1/offers/efficient`.

cURL

```
curl "https://pricevault.es/v1/offers/best-commission?consumption_mwh=10&power_kw=4.6&supply_type=LUZ&tariff_type=2.0TD&limit=10" \
  -H "X-API-Key: $PV_KEY"
```

## Comparar ofertas

**GET** `/v1/offers/compare` — Scope: offers:read

Devuelve el detalle completo de entre 2 y 10 ofertas para mostrarlas lado a lado. Repite el parámetro `ids` por cada oferta.

cURL

```
curl "https://pricevault.es/v1/offers/compare?ids=412&ids=488&ids=533" \
  -H "X-API-Key: $PV_KEY"
```

## Calcular comisión de una oferta

**GET** `/v1/offers/{id}/commission-calculator` — Scope: commissions:read

Calcula el importe exacto de comisión por consumo y/o potencia para una oferta concreta, indicando el tramo aplicado.

cURL

```
curl "https://pricevault.es/v1/offers/412/commission-calculator?consumption_mwh=10&power_kw=4.6" \
  -H "X-API-Key: $PV_KEY"
```

Respuesta 200

```
{
  "offer_id": 412,
  "consumption_mwh": 10,
  "power_kw": 4.6,
  "consumption_commission": {
    "value": 25.0, "commission_type": "fixed", "matched_tier": null
  },
  "power_commission": {
    "value": 6.0, "commission_type": "fixed",
    "matched_tier": { "power_from": 0, "power_to": 10 }
  }
}
```

## Detalle de una oferta

**GET** `/v1/offers/{id}` — Scope: offers:read

Devuelve la oferta completa, incluyendo tramos de comisión por consumo y potencia (a `null` sin `commissions:read`), precios indexados históricos y comunidades autónomas cubiertas.

cURL

```
curl "https://pricevault.es/v1/offers/412" \
  -H "X-API-Key: $PV_KEY"
```

Para la referencia exhaustiva de todos los endpoints y esquemas, consulta [ReDoc](https://pricevault.es/redoc).

## Crear y editar ofertas

Operaciones de escritura sobre una oferta individual. El borrado es lógico: la oferta queda inactiva, no se elimina.

**POST** `/v1/offers` — Crear una oferta. — offers:write

**PATCH** `/v1/offers/{id}` — Editar campos (parcial). — offers:write

**PATCH** `/v1/offers/{id}/toggle` — Activar / desactivar. — offers:write

**DELETE** `/v1/offers/{id}` — Desactivar (soft delete). — offers:write

Validaciones de negocio (**422**): al menos un territorio activo · al menos un tipo de cliente (DNI, CIF o comunidad de propietarios) · `fee_power` y `placas_solares` solo en `LUZ` · `precio_excedentes` requiere `placas_solares: true` · `ssaa_max_eur_mwh` solo en `LUZ`, requiere `ssaa_included: true` y debe estar entre 0 y 1000 · `permanencia_meses` (LUZ y GAS) debe ser un entero entre 1 y 60; `null` o `0` = sin permanencia (se guarda como `null`).

**POST** `/v1/offers` — Scope: offers:write

Crea una oferta. Puedes incluir tramos de comisión por consumo (`commission_tiers`) y por potencia (`commission_power_tiers`) en la misma llamada.

cURL

```
curl -X POST "https://pricevault.es/v1/offers" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "company_id": 12,
    "offer_name": "Tarifa Hogar 2.0TD",
    "supply_type": "LUZ",
    "tariff_type": "2.0TD",
    "territory_peninsula": true,
    "e1": 0.142, "e2": 0.108, "e3": 0.085,
    "p1": 0.084, "p2": 0.012,
    "commission": 25, "commission_type": "fixed",
    "acepta_dni": true, "acepta_cif": true, "acepta_cdad_propietarios": true,
    "commission_tiers": [
      { "consumption_from": 0,  "consumption_to": 10, "commission": 3, "commission_type": "fixed" },
      { "consumption_from": 10, "consumption_to": 50, "commission": 5, "commission_type": "fixed" }
    ]
  }'
```

## Carga y edición masiva

Dos formas de actualizar muchas ofertas a la vez. Ambas son todo-o-nada: si una fila falla, no se aplica ningún cambio.

**POST** `/v1/offers/bulk-patch` — Aplicar los mismos cambios a un lote (hasta 500 ofertas), vía JSON. — 30/min — offers:write

**POST** `/v1/offers/bulk-update` — Subir un XLSX (≤10 MB) con hojas de ofertas, indexados y borrados. Si una comercializadora del Excel coincide con una existente de nombre equivalente, las ofertas nuevas se asignan a esa y la respuesta lo lista en `reassigned_companies` (`[{"from": "EDP Energía SAU", "to": "EDP"}]`). — 10/hora — offers:write

### Edición masiva por JSON

cURL

```
curl -X POST "https://pricevault.es/v1/offers/bulk-patch" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "offer_ids": [412, 488, 533],
    "updates": { "is_active": false, "commission": 30 }
  }'
```

### Carga por archivo XLSX

cURL

```
curl -X POST "https://pricevault.es/v1/offers/bulk-update" \
  -H "X-API-Key: $PV_KEY" \
  -F "file=@tarifas.xlsx"
```

¿Necesitas la plantilla? Descárgala (precargada con tus ofertas) desde `GET /v1/offers/bulk-template` y `/bulk-template-indexed` (parámetro `client_id`). Como llevan las comisiones reales, piden además `commissions:read`.

Cada fila con `offer_id` reemplaza la oferta completa: **una celda vacía borra ese valor**. Parte siempre de la plantilla descargada, que viene rellena con los valores actuales.

## Precios indexados

Histórico mensual de precios de las ofertas indexadas (`is_indexed: true`). El upsert crea o reemplaza el mes indicado en `reference_month` (formato `YYYY-MM-DD`).

**GET** `/v1/offers/{id}/indexed-prices` — Listar el histórico de una oferta. — indexed_prices:read

**POST** `/v1/offers/{id}/indexed-prices` — Upsert del precio de un mes. — offers:write

**DELETE** `/v1/offers/{id}/indexed-prices/{month}` — Borrar el precio de un mes. — offers:write

cURL

```
curl -X POST "https://pricevault.es/v1/offers/488/indexed-prices" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_month": "2026-06-01",
    "e1": 0.154, "e2": 0.143, "e3": 0.121,
    "p1": 0.079, "p2": 0.065
  }'
```

## Comercializadoras

Las comercializadoras pertenecen a una asesoría (`client_id`). Dos nombres son «equivalentes» si son la misma marca escrita distinto (`EDP` y `EDP Energía SAU`). La lectura requiere el scope `companies:read`; la escritura, `companies:write`.

**GET** `/v1/companies` — Listar (paginado). — companies:read

**GET** `/v1/companies/{id}` — Detalle. — companies:read

**POST** `/v1/companies` — Crear (requiere `client_id` y `name`). `409` si el nombre existe o si hay una comercializadora equivalente (`detail.similar_company` con su `id` y `name`); añade `?force=true` para crearla igualmente. — companies:write

**PATCH** `/v1/companies/{id}` — Editar (también `is_active`). Mismo `409` / `?force=true` al renombrar a un nombre equivalente. — companies:write

**PATCH** `/v1/companies/{id}/toggle` — Activar / desactivar. Desactivada, sus ofertas dejan de salir en listados, rankings y estadísticas sin perder su propio estado. — companies:write

**DELETE** `/v1/companies/{id}` — Borrar para siempre la comercializadora y todas sus ofertas (`204`). — companies:write

cURL

```
curl -X POST "https://pricevault.es/v1/companies" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "client_id": 3, "name": "Energía X", "logo_url": null }'
```

## Asesorías (clientes)

Las asesorías son el primer nivel de tu organización: agrupan comercializadoras y ofertas. La lectura requiere el scope `clients:read`; la escritura, `clients:write`.

**GET** `/v1/clients` — Listar (paginado). — clients:read

**GET** `/v1/clients/{id}` — Detalle. — clients:read

**POST** `/v1/clients` — Crear. `409` si el nombre existe, `403` si superas el límite del plan. — clients:write

**PATCH** `/v1/clients/{id}` — Editar. — clients:write

**PATCH** `/v1/clients/{id}/toggle` — Activar / desactivar. — clients:write

**DELETE** `/v1/clients/{id}` — Desactivar. — clients:write

## Estadísticas y exportación

**GET** `/v1/offers/stats` — Totales por suministro, asesorías, comercializadoras y cargas. — offers:read

**GET** `/v1/offers/export` — Exportar ofertas a CSV (máx. 2000 filas). La columna `company_is_active` indica si la comercializadora está activa (con `include_inactive=true` salen también las de comercializadoras inactivas). — offers:export

cURL

```
curl "https://pricevault.es/v1/offers/stats" \
  -H "X-API-Key: $PV_KEY"
```

## Catálogo compartido

El catálogo central de ofertas y empresas reutilizables del que tus asesorías adoptan ofertas. Aquí cubrimos la lectura y la carga masiva por XLSX; el resto de la escritura (scope `catalog:write`, límite 120/min por key) está en la referencia completa.

**GET** `/v1/catalog/companies` — Empresas del catálogo (activas; `include_inactive=true` para todas). Se desactivan con `PATCH /v1/catalog/companies/{id}` y `{"is_active": false}`: sus ofertas desaparecen para las asesorías que las han adoptado mientras lo estén; `DELETE` las borra para siempre junto con las adopciones. — catalog:read

**GET** `/v1/catalog/offers` — Ofertas del catálogo, paginadas (filtros `company_id`, `supply_type`, `client_type`). — catalog:read

**GET** `/v1/catalog/offers/{id}/indexed-prices` — Histórico indexado de una oferta del catálogo. — catalog:read

cURL

```
curl "https://pricevault.es/v1/catalog/offers?supply_type=LUZ&client_type=cif" \
  -H "X-API-Key: $PV_KEY"
```

### Carga masiva por archivo XLSX

Crea, actualiza y borra ofertas del catálogo en bloque. Es **todo-o-nada**: si una fila falla, no se aplica ningún cambio. Los cambios llegan al instante a todas las asesorías que las han adoptado.

**GET** `/v1/catalog/offers/bulk-template` — Plantilla de ofertas fijas, precargada con el catálogo (filtros `supply_type`, `include_inactive`). — catalog:read

**GET** `/v1/catalog/offers/bulk-template-indexed` — Plantilla de ofertas indexadas, con la hoja `precios_indexados`. — catalog:read

**POST** `/v1/catalog/offers/bulk-update` — Subir un XLSX (≤10 MB) con hojas `ofertas`, `precios_indexados`, `tramos_consumo`, `tramos_potencia` y `borrar`. Si una comercializadora del Excel coincide con una existente de nombre equivalente, las ofertas nuevas se asignan a esa y la respuesta lo lista en `reassigned_companies` (`[{"from": "EDP Energía SAU", "to": "EDP"}]`). — 10/hora — catalog:write

cURL

```
curl -X POST "https://pricevault.es/v1/catalog/offers/bulk-update" \
  -H "X-API-Key: $PV_KEY" \
  -F "file=@catalogo.xlsx"
```

El formato es el de la carga de ofertas de asesoría, sin la columna `cliente` y con `catalog_offer_id` en lugar de `offer_id`. Una fila sin id da de alta la oferta (y la comercializadora, si no existe). Los precios indexados y los tramos de una oferta creada en la misma subida se enlazan por comercializadora y nombre de oferta, con la columna del id vacía.

Cada fila con `catalog_offer_id` reemplaza la oferta completa: **una celda vacía borra ese valor**. Parte siempre de la plantilla descargada, que viene rellena con los valores actuales.

El límite de 10 subidas por hora se comparte con la subida desde el panel. Si hay filas con errores responde `422` con la lista:

JSON

```
{
  "detail": {
    "message": "El fichero contiene errores. No se ha aplicado ningún cambio.",
    "errors": [
      { "row": 4, "sheet": "ofertas", "catalog_offer_id": 812, "error": "Debe marcarse al menos un territorio" }
    ]
  }
}
```

## Cuenta

Datos de la organización propietaria de la API key. Cualquier API key válida puede consultarlos; para modificarlos hace falta una key de **acceso completo** (sin scopes ni cliente asociado), o se responde `403`.

**GET** `/v1/account` — Datos de la cuenta. — sin scope

**PATCH** `/v1/account` — Actualizar nombre o email. Cambiar el email exige `current_password` (`401` si no coincide; `409` si el email ya existe) y queda pendiente hasta confirmarlo (`pending_email`). — 30/hora — acceso completo

**POST** `/v1/account/password` — Cambiar la contraseña (mínimo 8 caracteres). Cierra las sesiones web abiertas; las API keys no se ven afectadas. — 5/5min — acceso completo

cURL

```
curl "https://pricevault.es/v1/account" \
  -H "X-API-Key: $PV_KEY"
```

Respuesta 200

```
{
  "id": 3,
  "name": "Asesoría Norte",
  "email": "info@asesorianorte.es",
  "is_active": true,
  "created_at": "2026-01-15T09:00:00Z"
}
```

### Cambiar el email

El email es el identificador de acceso a la cuenta, así que cambiarlo pide también la contraseña actual. Para cambiar solo el nombre no hace falta.

El cambio no es inmediato: la respuesta devuelve el email nuevo en `pending_email` y `email` sigue siendo el actual hasta que el titular abre el enlace de confirmación que enviamos al email nuevo (24 h). Mientras tanto se sigue accediendo con el email antiguo, que recibe un aviso del cambio.

cURL

```
curl -X PATCH "https://pricevault.es/v1/account" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "nuevo@asesorianorte.es",
    "current_password": "••••••••"
  }'
```

### Cambiar la contraseña

cURL

```
curl -X POST "https://pricevault.es/v1/account/password" \
  -H "X-API-Key: $PV_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "current_password": "••••••••",
    "new_password": "••••••••"
  }'
```

Respuesta `204 No Content` si el cambio es correcto; `401` si la contraseña actual no coincide. Las sesiones web abiertas se cierran y se avisa por email al titular.

## Suscripción

El plan actual de la cuenta y sus límites. Devuelve `404` si la cuenta no tiene suscripción activa.

**GET** `/v1/subscription` — Plan, estado y límite de asesorías. — sin scope

Respuesta 200

```
{
  "id": 7,
  "plan": "pro",
  "status": "active",
  "clients_limit": 25,
  "trial_ends_at": null,
  "current_period_end": "2026-07-15T00:00:00Z",
  "created_at": "2026-01-15T09:00:00Z"
}
```

## Uso de la API

Telemetría de tus llamadas: registro detallado y resumen agregado. Útil para depurar integraciones y vigilar el consumo.

**GET** `/v1/usage` — Registro de llamadas. Filtros: `endpoint`, `status_code`, `from`, `to` (ISO 8601), paginado. — sin scope

**GET** `/v1/usage/summary` — Total de llamadas, desglose por endpoint y p95 de latencia. — sin scope

cURL

```
curl "https://pricevault.es/v1/usage/summary" \
  -H "X-API-Key: $PV_KEY"
```

Respuesta 200

```
{
  "total_calls": 18432,
  "calls_by_endpoint": [
    { "endpoint": "GET /v1/offers", "calls": 12045 },
    { "endpoint": "POST /v1/offers/efficient", "calls": 3210 }
  ],
  "p95_response_time_ms": 86
}
```

## Cargas

Seguimiento de las cargas de archivos. Tanto las subidas desde el dashboard como las cargas masivas por API (`bulk-update`) quedan registradas aquí.

**GET** `/v1/uploads` — Listar. Filtros: `status`, `client_id`, paginado. — sin scope

**GET** `/v1/uploads/{id}` — Detalle de una carga. — sin scope

**POST** `/v1/uploads` — Registrar una carga (queda en estado `pending`). — sin scope

**PATCH** `/v1/uploads/{id}` — Reintentar una carga fallida (`409` si no está `failed`). — sin scope

**DELETE** `/v1/uploads/{id}` — Cancelar una carga pendiente (`409` si no está `pending`). — sin scope

cURL

```
curl "https://pricevault.es/v1/uploads?status=completed&limit=20" \
  -H "X-API-Key: $PV_KEY"
```
