# API REST

La API REST de Frihet permite acceder y manipular los recursos de tu cuenta de forma programatica. Toda la comunicacion se realiza sobre HTTPS y las respuestas son JSON.

## URL base

```
https://api.frihet.io/v1
```

Todos los endpoints descritos en esta pagina son relativos a esta URL base.

**Descubrimiento:** Una peticion `GET` a la raiz (`https://api.frihet.io/`) devuelve los enlaces principales sin necesidad de autenticacion:

```json
{
  "name": "Frihet API",
  "version": "1.0.0",
  "docs": "https://docs.frihet.io/desarrolladores/api-rest",
  "openapi": "https://api.frihet.io/openapi.yaml",
  "mcp": "https://mcp.frihet.io",
  "status": "https://status.frihet.io"
}
```

La especificacion OpenAPI 3.1 esta disponible en `https://api.frihet.io/openapi.yaml`.

:::tip SDK disponible
Si usas TypeScript o JavaScript, el SDK oficial simplifica la integracion:

```bash
npm install @frihet/sdk
```

```typescript
const frihet = new Frihet({ apiKey: 'fri_...' });
const invoices = await frihet.invoices.list({ status: 'overdue' });
```

Repositorio: [github.com/Frihet-io/frihet-sdk](https://github.com/Frihet-io/frihet-sdk)
:::

---

## Autenticacion

Cada peticion debe incluir una API key en la cabecera `X-API-Key`. Las claves se crean desde **Ajustes > Desarrolladores > API Keys** en el panel de Frihet.

Las claves tienen el prefijo `fri_` seguido de 32 bytes aleatorios codificados en base64url. Ejemplo: `fri_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345678`.

```bash
curl https://api.frihet.io/v1/clients \
  -H "X-API-Key: fri_tu-clave-aqui"
```

Alternativamente, puedes enviar la clave como Bearer token en la cabecera `Authorization`:

```bash
curl https://api.frihet.io/v1/clients \
  -H "Authorization: Bearer fri_tu-clave-aqui"
```

### Seguridad de las claves

- La clave en texto plano solo se muestra **una vez**, en el momento de la creacion. No se puede recuperar despues.
- El servidor almacena un **hash SHA-256** de la clave. Incluso en caso de brecha de datos, la clave original no es recuperable.
- Puedes crear claves con **fecha de expiracion** configurable.
- Si sospechas que una clave ha sido comprometida, revocala inmediatamente desde el panel.

### Ciclo de vida de las claves

**Crear una clave:**

1. Ve a **Ajustes > Desarrolladores > API Keys**
2. Pulsa **Crear clave**
3. Asigna un nombre descriptivo (por ejemplo, `integracion-contabilidad`)
4. Opcionalmente, establece una **fecha de expiracion** en dias. Si lo dejas vacio, la clave no caduca
5. Copia la clave inmediatamente -- no podras verla de nuevo

**Expiracion:**

Las claves con fecha de expiracion dejan de funcionar automaticamente al cumplirse el plazo. Las peticiones con una clave expirada reciben un `401 Unauthorized`.

**Revocacion:**

Puedes revocar una clave en cualquier momento desde **Ajustes > Desarrolladores > API Keys**. La revocacion es **inmediata e irreversible**: las peticiones en curso con esa clave fallaran a partir de ese instante.

**Rotacion de claves:**

Para rotar una clave sin interrumpir el servicio:

1. Crea una nueva clave con el mismo alcance
2. Actualiza tu integracion para usar la nueva clave
3. Verifica que las peticiones funcionan correctamente
4. Revoca la clave anterior

**Monitorizacion:**

Cada clave muestra la fecha de **ultimo uso** en el panel de Ajustes. Revisa periodicamente las claves inactivas y revoca las que ya no se utilicen.

---

## Rate limiting

Cada clave API tiene un limite de **100 peticiones por minuto**. Si se excede, la API responde con un codigo `429`:

```json
{
  "error": "Rate limit exceeded",
  "message": "Maximum 100 requests per minute",
  "retryAfter": 60
}
```

### Cabeceras de rate limiting

Todas las respuestas de la API incluyen cabeceras para que puedas gestionar el limite de forma proactiva:

| Cabecera                | Descripcion                                             | Ejemplo      |
| ----------------------- | ------------------------------------------------------- | ------------ |
| `X-RateLimit-Limit`     | Peticiones permitidas por minuto                        | `100`        |
| `X-RateLimit-Remaining` | Peticiones restantes en la ventana actual               | `87`         |
| `X-RateLimit-Reset`     | Timestamp Unix (segundos) en que la ventana se reinicia | `1709312400` |

**Ejemplo de respuesta con cabeceras de rate limiting:**

```
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1709312400
Content-Type: application/json
```

**Gestion del codigo 429:**

Cuando recibes un `429`, usa las cabeceras para calcular cuanto esperar antes de reintentar:

```javascript
async function fetchWithRateLimit(url, options) {
  const response = await fetch(url, options);

  if (response.status === 429) {
    const resetTimestamp = response.headers.get('X-RateLimit-Reset');
    const waitMs = Number(resetTimestamp) * 1000 - Date.now();
    await new Promise((resolve) => setTimeout(resolve, Math.max(waitMs, 1000)));
    return fetch(url, options);
  }

  return response;
}
```

**Recomendaciones:**

- Si recibes un `429`, espera hasta el timestamp indicado en `X-RateLimit-Reset` antes de reintentar.
- Monitoriza `X-RateLimit-Remaining` para frenar las peticiones antes de alcanzar el limite.
- Distribuye las peticiones en el tiempo en lugar de enviar rafagas.

---

## Tamano de peticion

El cuerpo de las peticiones POST, PUT y PATCH no puede exceder **1 MB**. Peticiones mayores reciben un codigo `413`.

---

## Recursos

La API expone 8 recursos principales: invoices, expenses, clients, products, quotes, vendors, deposits y webhooks. Todos soportan operaciones CRUD completas (GET, POST, PUT/PATCH, DELETE). Ademas, los clientes tienen subcolecciones CRM: contacts, activities y notes.

:::info PATCH vs PUT
Tanto `PUT` como `PATCH` aceptan actualizaciones parciales. No necesitas enviar el recurso completo -- solo los campos que quieras modificar.
:::

### Facturas (`/invoices`)

#### Listar facturas

```
GET /v1/invoices
```

**Parametros de consulta:**

| Parametro  | Tipo    | Por defecto | Descripcion                                                                    |
| ---------- | ------- | ----------- | ------------------------------------------------------------------------------ |
| `limit`    | integer | 50          | Resultados por pagina (maximo 100)                                             |
| `offset`   | integer | 0           | Numero de resultados a saltar (maximo 10.000)                                  |
| `status`   | string  | --          | Filtrar por estado: `draft`, `sent`, `partial`, `paid`, `overdue`, `cancelled` |
| `from`     | string  | --          | Fecha inicio (ISO 8601: `YYYY-MM-DD`). Filtra por `issueDate`                  |
| `to`       | string  | --          | Fecha fin (ISO 8601: `YYYY-MM-DD`). Filtra por `issueDate`                     |
| `clientId` | string  | --          | Filtrar por ID de cliente                                                      |
| `seriesId` | string  | --          | Filtrar por serie de numeracion                                                |
| `q`        | string  | --          | Busqueda de texto (nombre de cliente, numero, notas)                           |
| `cursor`   | string  | --          | Cursor para paginacion basada en cursor (ignora `offset`)                      |
| `fields`   | string  | --          | Campos a devolver, separados por coma (siempre incluye `id`)                   |

**Ejemplo:**

```bash
curl "https://api.frihet.io/v1/invoices?limit=10&status=paid&from=2026-01-01&to=2026-03-31" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": [
    {
      "id": "abc123",
      "clientName": "Acme S.L.",
      "items": [{ "description": "Consultoria", "quantity": 10, "unitPrice": 75 }],
      "status": "paid",
      "issueDate": "2026-01-15",
      "dueDate": "2026-02-15",
      "taxRate": 21,
      "notes": "",
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-01-20T14:00:00.000Z"
    }
  ],
  "total": 42,
  "limit": 10,
  "offset": 0
}
```

#### Obtener factura

```
GET /v1/invoices/:id
```

```bash
curl https://api.frihet.io/v1/invoices/abc123 \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "id": "abc123",
  "clientName": "Acme S.L.",
  "items": [{ "description": "Consultoria", "quantity": 10, "unitPrice": 75 }],
  "status": "paid",
  "issueDate": "2026-01-15",
  "dueDate": "2026-02-15",
  "taxRate": 21,
  "notes": "",
  "createdAt": "2026-01-15T10:30:00.000Z",
  "updatedAt": "2026-01-20T14:00:00.000Z"
}
```

#### Crear factura

```
POST /v1/invoices
```

**Campos requeridos:**

| Campo        | Tipo   | Descripcion                                                                       |
| ------------ | ------ | --------------------------------------------------------------------------------- |
| `clientName` | string | Nombre del cliente (max 10.000 caracteres)                                        |
| `items`      | array  | Lista de lineas de la factura. Cada linea: `{ description, quantity, unitPrice }` |

**Campos opcionales:**

| Campo       | Tipo   | Descripcion                                                          |
| ----------- | ------ | -------------------------------------------------------------------- |
| `status`    | string | `draft` (defecto), `sent`, `partial`, `paid`, `overdue`, `cancelled` |
| `issueDate` | string | Fecha de emision (ISO 8601). Por defecto: hoy                        |
| `dueDate`   | string | Fecha de vencimiento (ISO 8601)                                      |
| `notes`     | string | Notas internas (max 10.000 caracteres)                               |
| `taxRate`   | number | Porcentaje de impuesto (0-100). Ej: `21` para IVA 21%                |

**Estructura de cada linea (`items[]`):**

| Campo         | Tipo   | Requerido | Descripcion                                      |
| ------------- | ------ | --------- | ------------------------------------------------ |
| `description` | string | Si        | Descripcion del concepto (max 10.000 caracteres) |
| `quantity`    | number | Si        | Cantidad                                         |
| `unitPrice`   | number | Si        | Precio unitario                                  |

```bash
curl -X POST https://api.frihet.io/v1/invoices \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "clientName": "Acme S.L.",
    "items": [
      { "description": "Desarrollo web", "quantity": 40, "unitPrice": 60 }
    ],
    "dueDate": "2026-03-01",
    "taxRate": 21,
    "notes": "Proyecto Q1 2026"
  }'
```

**Respuesta (201):**

```json
{
  "id": "def456",
  "clientName": "Acme S.L.",
  "items": [{ "description": "Desarrollo web", "quantity": 40, "unitPrice": 60 }],
  "status": "draft",
  "issueDate": "2026-02-12",
  "dueDate": "2026-03-01",
  "taxRate": 21,
  "notes": "Proyecto Q1 2026",
  "createdAt": "2026-02-12T09:00:00.000Z",
  "updatedAt": "2026-02-12T09:00:00.000Z"
}
```

#### Actualizar factura (PUT o PATCH)

```
PUT /v1/invoices/:id
PATCH /v1/invoices/:id
```

Solo necesitas enviar los campos que quieras modificar. Los campos no incluidos permanecen sin cambios.

```bash
curl -X PATCH https://api.frihet.io/v1/invoices/def456 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "sent",
    "notes": "Enviada al cliente"
  }'
```

**Respuesta (200):** Objeto de factura actualizado.

:::caution
Si envias `items`, debes enviar el array completo -- no se admiten actualizaciones parciales de lineas individuales.
:::

#### Eliminar factura

```
DELETE /v1/invoices/:id
```

```bash
curl -X DELETE https://api.frihet.io/v1/invoices/def456 \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta:** `204 No Content`

#### Descargar factura en PDF

```
GET /v1/invoices/:id/pdf
```

Devuelve el PDF de la factura como `application/pdf` con la cabecera `Content-Disposition: attachment`.

```bash
curl -o factura.pdf https://api.frihet.io/v1/invoices/abc123/pdf \
  -H "X-API-Key: fri_tu-clave-aqui"
```

#### Enviar factura por email

```
POST /v1/invoices/:id/send
```

Envia la factura al destinatario especificado a traves de Resend. Si la factura esta en estado `draft`, se actualiza automaticamente a `sent`.

**Campos:**

| Campo            | Tipo   | Requerido | Descripcion                                                         |
| ---------------- | ------ | --------- | ------------------------------------------------------------------- |
| `recipientEmail` | string | Si        | Email del destinatario (max 255 caracteres)                         |
| `recipientName`  | string | No        | Nombre del destinatario (max 200 caracteres)                        |
| `customMessage`  | string | No        | Mensaje personalizado en el cuerpo del email (max 5.000 caracteres) |
| `locale`         | string | No        | Idioma del email: `es` (defecto) o `en`                             |

```bash
curl -X POST https://api.frihet.io/v1/invoices/abc123/send \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "recipientEmail": "admin@acme.es",
    "recipientName": "Departamento de Contabilidad",
    "locale": "es"
  }'
```

**Respuesta (200):**

```json
{ "success": true, "messageId": "re_abc123..." }
```

#### Marcar factura como pagada

```
POST /v1/invoices/:id/paid
```

| Campo      | Tipo   | Requerido | Descripcion                                |
| ---------- | ------ | --------- | ------------------------------------------ |
| `paidDate` | string | No        | Fecha de pago (ISO 8601). Por defecto: hoy |

```bash
curl -X POST https://api.frihet.io/v1/invoices/abc123/paid \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{ "paidDate": "2026-03-15" }'
```

**Respuesta (200):**

```json
{ "success": true, "status": "paid", "paidAt": "2026-03-15" }
```

#### Crear factura rectificativa (abono)

```
POST /v1/invoices/:id/credit-note
```

Crea un **borrador** de abono o nota de credito a partir de una factura existente. El endpoint crea, no emite: no asigna numero fiscal, no calcula hash y no envia a VeriFactu. Esos pasos ocurren cuando una persona emite el borrador desde la aplicacion.

**Requisitos:**

- Plan `pro` o superior. Si no: `403 PLAN_UPGRADE_REQUIRED`.
- Cabecera `Idempotency-Key` **obligatoria** (a diferencia del resto de `POST`, donde es opcional). Si falta: `400 IDEMPOTENCY_KEY_REQUIRED`.
- La factura original no puede ser `draft`, ni estar `cancelled`, ni estar ya totalmente abonada.
- La factura original debe tener `clientLocation`. Si no: `400 ORIGINAL_INVOICE_MISSING_FISCAL_ZONE` — sin zona fiscal no se puede determinar la forma correcta de la rectificativa.

**Cuerpo** (se rechaza cualquier campo no listado):

| Campo               | Tipo    | Requerido | Descripcion                                                    |
| ------------------- | ------- | --------- | -------------------------------------------------------------- |
| `reason`            | string  | Si        | Motivo: `refund`, `discount`, `error`, `cancellation`, `other` |
| `reasonDescription` | string  | No        | Descripcion libre del motivo (max. 500 caracteres)             |
| `fullCredit`        | boolean | No        | Solo `true` (o se omite). Por defecto: `true`                  |
| `issueDate`         | string  | No        | Fecha de emision (YYYY-MM-DD). Por defecto: hoy                |

Los abonos **parciales no estan disponibles** en la API: `fullCredit: false` devuelve `400 PARTIAL_CREDIT_NOT_IMPLEMENTED`. Una rectificacion parcial necesita las lineas corregidas, y este endpoint no las acepta; hazla en la aplicacion.

La rectificacion es siempre **por diferencias** (`rectificationMethod: "I"`). El metodo `S` (por sustitucion) no se ofrece en la API: exigiria reexpresar tambien los importes finales corregidos, que este endpoint no puede expresar.

```bash
curl -X POST https://api.frihet.io/v1/invoices/abc123/credit-note \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "reason": "error",
    "reasonDescription": "Correccion de importe"
  }'
```

**Respuesta (201):**

```json
{
  "data": {
    "success": true,
    "creditNote": {
      "id": "xyz789",
      "documentNumber": "CN-F2026-0042",
      "originalInvoiceId": "abc123",
      "reason": "error",
      "fullCredit": true,
      "status": "draft",
      "rectificationMethod": "I",
      "totalCredited": 544.5
    }
  },
  "meta": {
    "requestId": "req_...",
    "timestamp": "2026-03-28T10:00:00.000Z"
  }
}
```

Un reintento con la **misma** `Idempotency-Key` devuelve este mismo `201`, con el mismo `id` y el mismo cuerpo byte a byte, mas la cabecera `X-Idempotent-Replayed: true`. No se crea un segundo borrador. Ver [Idempotencia](#idempotencia).

**Tipo R:** el tipo de rectificativa **no se envia**, se deriva del `reason`. La API no acepta un campo `type`.

| Motivo `reason` | Tipo R asignado | Descripcion legal                                               |
| --------------- | --------------- | --------------------------------------------------------------- |
| `error`         | `R1`            | Art. 80.1, 80.2 y 80.6 LIVA; art. 15 del Real Decreto 1619/2012 |
| `refund`        | `R4`            | Resto de causas (devolucion, descuento, anulacion)              |
| `discount`      | `R4`            | Resto de causas                                                 |
| `cancellation`  | `R4`            | Resto de causas                                                 |
| `other`         | `R4`            | Resto de causas                                                 |

Los tipos `R2` (art. 80.3, concurso de acreedores), `R3` (art. 80.4, creditos incobrables) y `R5` (facturas simplificadas, art. 7.2 del RD 1619/2012) existen en la norma pero no se pueden seleccionar desde la API; usa la aplicacion.

#### Descargar factura en XML (EN16931)

```
GET /v1/invoices/:id/xml
```

Devuelve la factura como fichero XML de e-factura EN16931 en formato UBL 2.1 o CII (Cross Industry Invoice), segun la configuracion de la cuenta.

**Auth:** API key requerida (`X-API-Key`).

**Respuesta:** `application/xml` con cabecera `Content-Disposition: attachment`.

```bash
curl -o factura.xml "https://api.frihet.io/v1/invoices/abc123/xml" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200) — ejemplo UBL 2.1:**

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
         xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
         xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
  <cbc:CustomizationID>urn:cen.eu:en16931:2017</cbc:CustomizationID>
  <cbc:ID>F2026-0042</cbc:ID>
  <cbc:IssueDate>2026-01-15</cbc:IssueDate>
  <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
  <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
  <!-- ... -->
</Invoice>
```

**Errores posibles:**

| Codigo | Descripcion                                                                            |
| ------ | -------------------------------------------------------------------------------------- |
| `404`  | Factura no encontrada                                                                  |
| `422`  | La factura no tiene datos suficientes para generar el XML (p.ej., sin NIF del cliente) |

:::info Formato del XML
El formato (UBL 2.1 o CII) se configura en **Ajustes > Facturacion > E-factura**. UBL 2.1 es el formato por defecto y el requerido para VeriFactu y Facturae en Espana.
:::

#### Aplicar recargo por demora

```
POST /v1/invoices/:id/late-fee
```

Aplica un recargo por demora a una factura vencida. Genera una nota de debito con el importe calculado segun la configuracion de recargos de la cuenta (**Ajustes > Facturacion > Recargos por demora**).

**Auth:** API key requerida (`X-API-Key`).

**Requisito:** La factura debe estar en estado `overdue`. Si la factura no esta vencida, la API devuelve `422`.

**Body:** `{}` (vacio). Los parametros del recargo se toman de la configuracion de la cuenta.

```bash
curl -X POST "https://api.frihet.io/v1/invoices/abc123/late-fee" \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Respuesta (201):**

```json
{
  "success": true,
  "lateFee": {
    "id": "lf_abc789",
    "documentNumber": "DR-F2026-0001",
    "originalInvoiceId": "abc123",
    "type": "debit_note",
    "feeAmount": 18.15,
    "currency": "EUR",
    "daysOverdue": 22,
    "issueDate": "2026-03-28",
    "createdAt": "2026-03-28T10:00:00.000Z"
  }
}
```

**Errores posibles:**

| Codigo | Descripcion                                                                    |
| ------ | ------------------------------------------------------------------------------ |
| `404`  | Factura no encontrada                                                          |
| `422`  | La factura no esta en estado `overdue` o ya tiene un recargo por demora activo |

:::tip Configuracion de recargos
Define el porcentaje anual o importe fijo en **Ajustes > Facturacion > Recargos por demora**. El calculo usa la Ley 3/2004 (LMOC) para operaciones entre empresas por defecto.
:::

---

### Gastos (`/expenses`)

#### Listar gastos

```
GET /v1/expenses
```

**Parametros de consulta:**

| Parametro  | Tipo    | Por defecto | Descripcion                                        |
| ---------- | ------- | ----------- | -------------------------------------------------- |
| `limit`    | integer | 50          | Resultados por pagina (maximo 100)                 |
| `offset`   | integer | 0           | Numero de resultados a saltar                      |
| `from`     | string  | --          | Fecha inicio (ISO 8601). Filtra por `date`         |
| `to`       | string  | --          | Fecha fin (ISO 8601). Filtra por `date`            |
| `category` | string  | --          | Filtrar por categoria                              |
| `vendorId` | string  | --          | Filtrar por ID de proveedor                        |
| `q`        | string  | --          | Busqueda de texto (proveedor, descripcion, numero) |
| `cursor`   | string  | --          | Cursor para paginacion basada en cursor            |
| `fields`   | string  | --          | Campos a devolver, separados por coma              |

```bash
curl "https://api.frihet.io/v1/expenses?limit=20&from=2026-01-01" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": [
    {
      "id": "exp789",
      "description": "Licencia Adobe Creative Cloud",
      "amount": 59.99,
      "category": "software",
      "date": "2026-02-01",
      "vendor": "Adobe Inc.",
      "taxDeductible": true,
      "createdAt": "2026-02-01T10:00:00.000Z",
      "updatedAt": "2026-02-01T10:00:00.000Z"
    }
  ],
  "total": 15,
  "limit": 20,
  "offset": 0
}
```

#### Obtener gasto

```
GET /v1/expenses/:id
```

#### Crear gasto

```
POST /v1/expenses
```

**Campos requeridos:**

| Campo         | Tipo   | Descripcion                                   |
| ------------- | ------ | --------------------------------------------- |
| `description` | string | Descripcion del gasto (max 10.000 caracteres) |
| `amount`      | number | Importe del gasto                             |

**Campos opcionales:**

| Campo           | Tipo    | Descripcion                                  |
| --------------- | ------- | -------------------------------------------- |
| `category`      | string  | Categoria del gasto (max 10.000 caracteres)  |
| `date`          | string  | Fecha del gasto (ISO 8601). Por defecto: hoy |
| `vendor`        | string  | Proveedor (max 10.000 caracteres)            |
| `taxDeductible` | boolean | Si el gasto es deducible                     |

```bash
curl -X POST https://api.frihet.io/v1/expenses \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Licencia Adobe Creative Cloud",
    "amount": 59.99,
    "category": "software",
    "date": "2026-02-01",
    "vendor": "Adobe Inc.",
    "taxDeductible": true
  }'
```

**Respuesta (201):**

```json
{
  "id": "exp789",
  "description": "Licencia Adobe Creative Cloud",
  "amount": 59.99,
  "category": "software",
  "date": "2026-02-01",
  "vendor": "Adobe Inc.",
  "taxDeductible": true,
  "createdAt": "2026-02-12T09:15:00.000Z",
  "updatedAt": "2026-02-12T09:15:00.000Z"
}
```

#### Actualizar gasto (PUT o PATCH)

```
PUT /v1/expenses/:id
PATCH /v1/expenses/:id
```

```bash
curl -X PATCH https://api.frihet.io/v1/expenses/exp789 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 65.99, "taxDeductible": false }'
```

**Respuesta (200):** Objeto de gasto actualizado.

#### Eliminar gasto

```
DELETE /v1/expenses/:id
```

**Respuesta:** `204 No Content`

#### Marcar gasto como facturable

```
POST /v1/expenses/:id/billable
```

Vincula un gasto a un cliente para que se pueda facturar. Opcionalmente aplica un markup.

| Campo      | Tipo   | Requerido | Descripcion                    |
| ---------- | ------ | --------- | ------------------------------ |
| `clientId` | string | Si        | ID del cliente al que facturar |
| `markup`   | number | No        | Porcentaje de markup (0-1000%) |

```bash
curl -X POST https://api.frihet.io/v1/expenses/exp789/billable \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{ "clientId": "cli001", "markup": 15 }'
```

Para desvincular el gasto:

```
DELETE /v1/expenses/:id/billable
```

---

### Clientes (`/clients`)

#### Listar clientes

```
GET /v1/clients
```

**Parametros de consulta:**

| Parametro | Tipo    | Por defecto | Descripcion                                                                                   |
| --------- | ------- | ----------- | --------------------------------------------------------------------------------------------- |
| `limit`   | integer | 50          | Resultados por pagina (maximo 100)                                                            |
| `offset`  | integer | 0           | Numero de resultados a saltar                                                                 |
| `from`    | string  | --          | Fecha inicio (ISO 8601). Filtra por `createdAt`                                               |
| `to`      | string  | --          | Fecha fin (ISO 8601). Filtra por `createdAt`                                                  |
| `stage`   | string  | --          | Filtrar por etapa del pipeline: `lead`, `contacted`, `proposal`, `active`, `inactive`, `lost` |
| `q`       | string  | --          | Busqueda de texto (nombre, email, NIF)                                                        |
| `cursor`  | string  | --          | Cursor para paginacion basada en cursor                                                       |
| `fields`  | string  | --          | Campos a devolver, separados por coma                                                         |

```bash
curl "https://api.frihet.io/v1/clients?limit=50" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

#### Obtener cliente

```
GET /v1/clients/:id
```

#### Crear cliente

```
POST /v1/clients
```

**Campos requeridos:**

| Campo  | Tipo   | Descripcion                                |
| ------ | ------ | ------------------------------------------ |
| `name` | string | Nombre del cliente (max 10.000 caracteres) |

**Campos opcionales:**

| Campo     | Tipo   | Descripcion                      |
| --------- | ------ | -------------------------------- |
| `email`   | string | Email de contacto                |
| `phone`   | string | Telefono                         |
| `taxId`   | string | NIF/CIF/VAT                      |
| `address` | object | Direccion (ver estructura abajo) |

**Estructura de `address`:**

| Campo        | Tipo   | Descripcion        |
| ------------ | ------ | ------------------ |
| `street`     | string | Calle y numero     |
| `city`       | string | Ciudad             |
| `state`      | string | Provincia o estado |
| `postalCode` | string | Codigo postal      |
| `country`    | string | Pais               |

Todos los campos de `address` son opcionales.

```bash
curl -X POST https://api.frihet.io/v1/clients \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme S.L.",
    "email": "admin@acme.es",
    "taxId": "B12345678",
    "address": {
      "street": "Calle Gran Via 42",
      "city": "Madrid",
      "postalCode": "28013",
      "country": "ES"
    }
  }'
```

**Respuesta (201):**

```json
{
  "id": "cli001",
  "name": "Acme S.L.",
  "email": "admin@acme.es",
  "taxId": "B12345678",
  "address": {
    "street": "Calle Gran Via 42",
    "city": "Madrid",
    "postalCode": "28013",
    "country": "ES"
  },
  "createdAt": "2026-02-12T09:30:00.000Z",
  "updatedAt": "2026-02-12T09:30:00.000Z"
}
```

#### Actualizar cliente (PUT o PATCH)

```
PUT /v1/clients/:id
PATCH /v1/clients/:id
```

```bash
curl -X PATCH https://api.frihet.io/v1/clients/cli001 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+34 912 345 678" }'
```

**Respuesta (200):** Objeto de cliente actualizado.

#### Eliminar cliente

```
DELETE /v1/clients/:id
```

**Respuesta:** `204 No Content`

---

### CRM: Personas de contacto, Actividades y Notas

Los clientes tienen tres subcolecciones para gestionar relaciones CRM: personas de contacto, actividades y notas. Todos los endpoints requieren un `clientId` valido en la URL.

#### Personas de contacto (`/v1/clients/:id/contacts`)

##### Listar contactos

```
GET /v1/clients/:id/contacts
```

```bash
curl "https://api.frihet.io/v1/clients/cli001/contacts" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

##### Obtener contacto

```
GET /v1/clients/:id/contacts/:contactId
```

```bash
curl "https://api.frihet.io/v1/clients/cli001/contacts/con001" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

##### Crear contacto

```
POST /v1/clients/:id/contacts
```

**Campos requeridos:**

| Campo  | Tipo   | Descripcion                      |
| ------ | ------ | -------------------------------- |
| `name` | string | Nombre de la persona de contacto |

**Campos opcionales:**

| Campo       | Tipo    | Descripcion                             |
| ----------- | ------- | --------------------------------------- |
| `email`     | string  | Email del contacto                      |
| `phone`     | string  | Telefono                                |
| `role`      | string  | Cargo o rol (ej. "Director financiero") |
| `isPrimary` | boolean | Si es el contacto principal del cliente |

```bash
curl -X POST https://api.frihet.io/v1/clients/cli001/contacts \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Maria Garcia",
    "email": "maria@acme.es",
    "phone": "+34 612 345 678",
    "role": "Directora financiera",
    "isPrimary": true
  }'
```

**Respuesta (201):**

```json
{
  "id": "con001",
  "name": "Maria Garcia",
  "email": "maria@acme.es",
  "phone": "+34 612 345 678",
  "role": "Directora financiera",
  "isPrimary": true,
  "createdAt": "2026-03-15T10:00:00.000Z",
  "updatedAt": "2026-03-15T10:00:00.000Z"
}
```

##### Actualizar contacto

```
PATCH /v1/clients/:id/contacts/:contactId
```

```bash
curl -X PATCH https://api.frihet.io/v1/clients/cli001/contacts/con001 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{ "role": "CEO" }'
```

**Respuesta (200):** Objeto de contacto actualizado.

##### Eliminar contacto

```
DELETE /v1/clients/:id/contacts/:contactId
```

```bash
curl -X DELETE https://api.frihet.io/v1/clients/cli001/contacts/con001 \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta:** `204 No Content`

#### Actividades (`/v1/clients/:id/activities`)

El timeline de actividades registra las interacciones con un cliente. Las actividades del sistema (como `invoice_created`, `quote_sent`, etc.) se generan automaticamente. Tambien puedes crear actividades manuales.

:::caution Inmutables
Las actividades son inmutables. No se pueden actualizar ni eliminar una vez creadas.
:::

##### Listar actividades

```
GET /v1/clients/:id/activities
```

```bash
curl "https://api.frihet.io/v1/clients/cli001/activities" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

##### Obtener actividad

```
GET /v1/clients/:id/activities/:activityId
```

```bash
curl "https://api.frihet.io/v1/clients/cli001/activities/act001" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

##### Crear actividad

```
POST /v1/clients/:id/activities
```

**Campos requeridos:**

| Campo   | Tipo   | Descripcion                                            |
| ------- | ------ | ------------------------------------------------------ |
| `type`  | string | Tipo de actividad: `call`, `email`, `meeting` o `task` |
| `title` | string | Titulo descriptivo de la actividad                     |

**Campos opcionales:**

| Campo         | Tipo   | Descripcion                        |
| ------------- | ------ | ---------------------------------- |
| `description` | string | Descripcion detallada              |
| `metadata`    | object | Datos adicionales en formato libre |

:::info Tipos de actividad
Los tipos `call`, `email`, `meeting` y `task` son para actividades creadas manualmente. Los tipos del sistema como `invoice_created`, `quote_sent` o `expense_linked` se generan automaticamente y no se pueden crear via API.
:::

```bash
curl -X POST https://api.frihet.io/v1/clients/cli001/activities \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "call",
    "title": "Llamada de seguimiento presupuesto Q2",
    "description": "Comentamos las condiciones del presupuesto. Pendiente de confirmar.",
    "metadata": {
      "duration": "15min",
      "outcome": "pending"
    }
  }'
```

**Respuesta (201):**

```json
{
  "id": "act001",
  "type": "call",
  "title": "Llamada de seguimiento presupuesto Q2",
  "description": "Comentamos las condiciones del presupuesto. Pendiente de confirmar.",
  "metadata": {
    "duration": "15min",
    "outcome": "pending"
  },
  "createdAt": "2026-03-15T14:30:00.000Z"
}
```

#### Notas (`/v1/clients/:id/notes`)

##### Listar notas

```
GET /v1/clients/:id/notes
```

```bash
curl "https://api.frihet.io/v1/clients/cli001/notes" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

##### Obtener nota

```
GET /v1/clients/:id/notes/:noteId
```

```bash
curl "https://api.frihet.io/v1/clients/cli001/notes/note001" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

##### Crear nota

```
POST /v1/clients/:id/notes
```

**Campos requeridos:**

| Campo     | Tipo   | Descripcion          |
| --------- | ------ | -------------------- |
| `content` | string | Contenido de la nota |

```bash
curl -X POST https://api.frihet.io/v1/clients/cli001/notes \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Cliente interesado en plan Premium. Contactar en abril para renovacion."
  }'
```

**Respuesta (201):**

```json
{
  "id": "note001",
  "content": "Cliente interesado en plan Premium. Contactar en abril para renovacion.",
  "createdAt": "2026-03-15T16:00:00.000Z",
  "updatedAt": "2026-03-15T16:00:00.000Z"
}
```

##### Actualizar nota

```
PATCH /v1/clients/:id/notes/:noteId
```

```bash
curl -X PATCH https://api.frihet.io/v1/clients/cli001/notes/note001 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Cliente interesado en plan Premium. Reunion confirmada 5 de abril."
  }'
```

**Respuesta (200):** Objeto de nota actualizado.

##### Eliminar nota

```
DELETE /v1/clients/:id/notes/:noteId
```

```bash
curl -X DELETE https://api.frihet.io/v1/clients/cli001/notes/note001 \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta:** `204 No Content`

---

### Productos (`/products`)

#### Listar productos

```
GET /v1/products
```

Acepta `limit`, `offset`, `from` y `to` (filtra por `createdAt`).

#### Obtener producto

```
GET /v1/products/:id
```

#### Crear producto

```
POST /v1/products
```

**Campos requeridos:**

| Campo       | Tipo   | Descripcion                                            |
| ----------- | ------ | ------------------------------------------------------ |
| `name`      | string | Nombre del producto o servicio (max 10.000 caracteres) |
| `unitPrice` | number | Precio unitario                                        |

**Campos opcionales:**

| Campo         | Tipo   | Descripcion                         |
| ------------- | ------ | ----------------------------------- |
| `description` | string | Descripcion (max 10.000 caracteres) |
| `taxRate`     | number | Porcentaje de impuesto (0-100)      |

```bash
curl -X POST https://api.frihet.io/v1/products \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Hora de consultoria",
    "unitPrice": 75,
    "description": "Consultoria estrategica",
    "taxRate": 21
  }'
```

**Respuesta (201):**

```json
{
  "id": "prod001",
  "name": "Hora de consultoria",
  "unitPrice": 75,
  "description": "Consultoria estrategica",
  "taxRate": 21,
  "createdAt": "2026-02-12T10:00:00.000Z",
  "updatedAt": "2026-02-12T10:00:00.000Z"
}
```

#### Actualizar producto (PUT o PATCH)

```
PUT /v1/products/:id
PATCH /v1/products/:id
```

```bash
curl -X PATCH https://api.frihet.io/v1/products/prod001 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{ "unitPrice": 85 }'
```

**Respuesta (200):** Objeto de producto actualizado.

#### Eliminar producto

```
DELETE /v1/products/:id
```

**Respuesta:** `204 No Content`

---

### Presupuestos (`/quotes`)

#### Listar presupuestos

```
GET /v1/quotes
```

**Parametros de consulta:**

| Parametro | Tipo    | Por defecto | Descripcion                                                            |
| --------- | ------- | ----------- | ---------------------------------------------------------------------- |
| `limit`   | integer | 50          | Resultados por pagina (maximo 100)                                     |
| `offset`  | integer | 0           | Numero de resultados a saltar                                          |
| `status`  | string  | --          | Filtrar por estado: `draft`, `sent`, `accepted`, `rejected`, `expired` |
| `from`    | string  | --          | Fecha inicio (ISO 8601). Filtra por `issueDate`                        |
| `to`      | string  | --          | Fecha fin (ISO 8601). Filtra por `issueDate`                           |

```bash
curl "https://api.frihet.io/v1/quotes?status=sent" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

#### Obtener presupuesto

```
GET /v1/quotes/:id
```

#### Crear presupuesto

```
POST /v1/quotes
```

**Campos requeridos:**

| Campo        | Tipo   | Descripcion                                                                |
| ------------ | ------ | -------------------------------------------------------------------------- |
| `clientName` | string | Nombre del cliente (max 10.000 caracteres)                                 |
| `items`      | array  | Lineas del presupuesto. Cada linea: `{ description, quantity, unitPrice }` |

**Campos opcionales:**

| Campo        | Tipo   | Descripcion                                                  |
| ------------ | ------ | ------------------------------------------------------------ |
| `validUntil` | string | Fecha de validez (ISO 8601)                                  |
| `notes`      | string | Notas o condiciones (max 10.000 caracteres)                  |
| `status`     | string | `draft` (defecto), `sent`, `accepted`, `rejected`, `expired` |

```bash
curl -X POST https://api.frihet.io/v1/quotes \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "clientName": "Design Studio SL",
    "items": [
      { "description": "Desarrollo web", "quantity": 80, "unitPrice": 60 },
      { "description": "Diseno UX", "quantity": 20, "unitPrice": 55 }
    ],
    "validUntil": "2026-04-01",
    "notes": "Incluye 2 rondas de revision"
  }'
```

**Respuesta (201):**

```json
{
  "id": "quo001",
  "clientName": "Design Studio SL",
  "items": [
    { "description": "Desarrollo web", "quantity": 80, "unitPrice": 60 },
    { "description": "Diseno UX", "quantity": 20, "unitPrice": 55 }
  ],
  "status": "draft",
  "validUntil": "2026-04-01",
  "notes": "Incluye 2 rondas de revision",
  "createdAt": "2026-02-12T11:00:00.000Z",
  "updatedAt": "2026-02-12T11:00:00.000Z"
}
```

#### Actualizar presupuesto (PUT o PATCH)

```
PUT /v1/quotes/:id
PATCH /v1/quotes/:id
```

#### Eliminar presupuesto

```
DELETE /v1/quotes/:id
```

**Respuesta:** `204 No Content`

#### Descargar presupuesto en PDF

```
GET /v1/quotes/:id/pdf
```

Funciona igual que `/invoices/:id/pdf`. Devuelve `application/pdf`.

```bash
curl -o presupuesto.pdf https://api.frihet.io/v1/quotes/quo001/pdf \
  -H "X-API-Key: fri_tu-clave-aqui"
```

#### Enviar presupuesto por email

```
POST /v1/quotes/:id/send
```

Mismos campos que `/invoices/:id/send` (`recipientEmail`, `recipientName`, `customMessage`, `locale`). Si el presupuesto esta en estado `draft`, se actualiza automaticamente a `sent`.

---

### Proveedores (`/vendors`)

#### Listar proveedores

```
GET /v1/vendors
```

Acepta `limit`, `offset`, `from`, `to` (filtra por `createdAt`), `q`, `cursor`, `fields`.

#### Obtener proveedor

```
GET /v1/vendors/:id
```

#### Crear proveedor

```
POST /v1/vendors
```

**Campos requeridos:**

| Campo  | Tipo   | Descripcion          |
| ------ | ------ | -------------------- |
| `name` | string | Nombre del proveedor |

**Campos opcionales:**

| Campo     | Tipo   | Descripcion                                                               |
| --------- | ------ | ------------------------------------------------------------------------- |
| `email`   | string | Email de contacto                                                         |
| `phone`   | string | Telefono                                                                  |
| `taxId`   | string | NIF/CIF/VAT                                                               |
| `address` | object | Direccion (`street`, `city`, `province`, `zip`, `country`, `countryCode`) |

```bash
curl -X POST https://api.frihet.io/v1/vendors \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Adobe Inc.",
    "email": "billing@adobe.com",
    "taxId": "US87-1166924"
  }'
```

#### Actualizar proveedor (PUT o PATCH)

```
PUT /v1/vendors/:id
PATCH /v1/vendors/:id
```

#### Eliminar proveedor

```
DELETE /v1/vendors/:id
```

**Respuesta:** `204 No Content`

---

### Depositos (`/deposits`)

Los depositos permiten registrar pagos anticipados o senales de clientes antes de emitir la factura final. Un deposito puede aplicarse total o parcialmente a una o varias facturas, o reembolsarse al cliente.

#### Listar depositos

```
GET /v1/deposits
```

**Parametros de consulta:**

| Parametro  | Tipo    | Por defecto | Descripcion                                                     |
| ---------- | ------- | ----------- | --------------------------------------------------------------- |
| `limit`    | integer | 50          | Resultados por pagina (maximo 100)                              |
| `offset`   | integer | 0           | Numero de resultados a saltar                                   |
| `clientId` | string  | --          | Filtrar por ID de cliente                                       |
| `status`   | string  | --          | Filtrar por estado: `pending`, `partial`, `applied`, `refunded` |
| `from`     | string  | --          | Fecha inicio (ISO 8601: `YYYY-MM-DD`). Filtra por `date`        |
| `to`       | string  | --          | Fecha fin (ISO 8601: `YYYY-MM-DD`). Filtra por `date`           |
| `cursor`   | string  | --          | Cursor para paginacion basada en cursor (ignora `offset`)       |
| `fields`   | string  | --          | Campos a devolver, separados por coma (siempre incluye `id`)    |

**Ejemplo:**

```bash
curl "https://api.frihet.io/v1/deposits?clientId=cli001&status=pending" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": [
    {
      "id": "dep001",
      "clientId": "cli001",
      "clientName": "Acme S.L.",
      "amount": 2000,
      "currency": "EUR",
      "date": "2026-03-15",
      "description": "Senal proyecto Q2",
      "paymentMethod": "bank_transfer",
      "reference": "TRF-20260315-001",
      "status": "pending",
      "appliedAmount": 0,
      "remainingAmount": 2000,
      "notes": "",
      "createdAt": "2026-03-15T10:00:00.000Z",
      "updatedAt": "2026-03-15T10:00:00.000Z"
    }
  ],
  "total": 5,
  "limit": 50,
  "offset": 0
}
```

#### Obtener deposito

```
GET /v1/deposits/{id}
```

```bash
curl https://api.frihet.io/v1/deposits/dep001 \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "id": "dep001",
  "clientId": "cli001",
  "clientName": "Acme S.L.",
  "amount": 2000,
  "currency": "EUR",
  "date": "2026-03-15",
  "description": "Senal proyecto Q2",
  "paymentMethod": "bank_transfer",
  "reference": "TRF-20260315-001",
  "status": "pending",
  "appliedAmount": 0,
  "remainingAmount": 2000,
  "notes": "",
  "createdAt": "2026-03-15T10:00:00.000Z",
  "updatedAt": "2026-03-15T10:00:00.000Z"
}
```

#### Crear deposito

```
POST /v1/deposits
```

**Campos requeridos:**

| Campo      | Tipo   | Descripcion                              |
| ---------- | ------ | ---------------------------------------- |
| `clientId` | string | ID del cliente                           |
| `amount`   | number | Importe del deposito (debe ser positivo) |

**Campos opcionales:**

| Campo           | Tipo   | Descripcion                                                   |
| --------------- | ------ | ------------------------------------------------------------- |
| `currency`      | string | Codigo ISO 4217 (3 caracteres). Por defecto: `EUR`            |
| `date`          | string | Fecha del deposito (ISO 8601: `YYYY-MM-DD`). Por defecto: hoy |
| `description`   | string | Descripcion o concepto del deposito (max 10.000 caracteres)   |
| `paymentMethod` | string | Metodo de pago: `bank_transfer`, `card`, `cash`, `other`      |
| `reference`     | string | Referencia del pago (max 500 caracteres)                      |
| `notes`         | string | Notas internas (max 10.000 caracteres)                        |

```bash
curl -X POST https://api.frihet.io/v1/deposits \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "cli001",
    "amount": 2000,
    "currency": "EUR",
    "date": "2026-03-15",
    "description": "Senal proyecto Q2",
    "paymentMethod": "bank_transfer",
    "reference": "TRF-20260315-001",
    "notes": "Pago anticipado confirmado por email"
  }'
```

**Respuesta (201):**

```json
{
  "id": "dep001",
  "clientId": "cli001",
  "clientName": "Acme S.L.",
  "amount": 2000,
  "currency": "EUR",
  "date": "2026-03-15",
  "description": "Senal proyecto Q2",
  "paymentMethod": "bank_transfer",
  "reference": "TRF-20260315-001",
  "status": "pending",
  "appliedAmount": 0,
  "remainingAmount": 2000,
  "notes": "Pago anticipado confirmado por email",
  "createdAt": "2026-03-15T10:00:00.000Z",
  "updatedAt": "2026-03-15T10:00:00.000Z"
}
```

#### Actualizar deposito (PATCH)

```
PATCH /v1/deposits/{id}
```

Solo necesitas enviar los campos que quieras modificar. Los campos no incluidos permanecen sin cambios.

```bash
curl -X PATCH https://api.frihet.io/v1/deposits/dep001 \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Senal proyecto Q2 — actualizada",
    "notes": "Confirmado con contrato firmado"
  }'
```

**Respuesta (200):** Objeto de deposito actualizado.

#### Eliminar deposito

```
DELETE /v1/deposits/{id}
```

```bash
curl -X DELETE https://api.frihet.io/v1/deposits/dep001 \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta:** `204 No Content`

:::caution
Solo se pueden eliminar depositos en estado `pending`. Si el deposito ya ha sido aplicado parcial o totalmente, la API devuelve `422`.
:::

#### Aplicar deposito a una factura

```
POST /v1/deposits/{id}/apply
```

Descuenta el deposito (total o parcial) del importe de una factura. Si el importe aplicado agota el saldo del deposito, el estado cambia a `applied`. Si queda saldo restante, el estado pasa a `partial`.

| Campo       | Tipo   | Requerido | Descripcion                                           |
| ----------- | ------ | --------- | ----------------------------------------------------- |
| `invoiceId` | string | Si        | ID de la factura a la que aplicar el deposito         |
| `amount`    | number | Si        | Importe a aplicar. No puede exceder `remainingAmount` |

```bash
curl -X POST https://api.frihet.io/v1/deposits/dep001/apply \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "invoiceId": "abc123",
    "amount": 1500
  }'
```

**Respuesta (200):**

```json
{
  "success": true,
  "deposit": {
    "id": "dep001",
    "status": "partial",
    "appliedAmount": 1500,
    "remainingAmount": 500
  },
  "invoice": {
    "id": "abc123",
    "status": "partial",
    "amountDue": 800
  }
}
```

**Errores posibles:**

| Codigo | Descripcion                                                                    |
| ------ | ------------------------------------------------------------------------------ |
| `404`  | Deposito o factura no encontrados                                              |
| `422`  | El importe supera el saldo disponible, o la factura ya esta pagada o cancelada |

#### Reembolsar deposito

```
POST /v1/deposits/{id}/refund
```

Registra la devolucion total o parcial del deposito al cliente. El estado del deposito cambia a `refunded`.

| Campo    | Tipo   | Requerido | Descripcion                                                                   |
| -------- | ------ | --------- | ----------------------------------------------------------------------------- |
| `amount` | number | No        | Importe a reembolsar. Si se omite, se reembolsa el `remainingAmount` completo |
| `reason` | string | No        | Motivo del reembolso (max 1.000 caracteres)                                   |

```bash
curl -X POST https://api.frihet.io/v1/deposits/dep001/refund \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500,
    "reason": "Proyecto cancelado por el cliente"
  }'
```

**Respuesta (200):**

```json
{
  "success": true,
  "deposit": {
    "id": "dep001",
    "status": "refunded",
    "appliedAmount": 1500,
    "remainingAmount": 0,
    "refundedAmount": 500
  }
}
```

**Errores posibles:**

| Codigo | Descripcion                                                                              |
| ------ | ---------------------------------------------------------------------------------------- |
| `404`  | Deposito no encontrado                                                                   |
| `422`  | El importe supera el saldo disponible o el deposito ya ha sido reembolsado completamente |

---

## Operaciones por lote (`/batch`)

Todos los recursos principales soportan creacion en lote. Envia un array de hasta 50 elementos en una sola peticion.

```
POST /v1/{resource}/batch
```

**Recursos soportados:** `invoices`, `expenses`, `clients`, `vendors`, `products`, `quotes`, `deposits`

**Cuerpo de la peticion:**

```json
{
  "items": [
    {
      "clientName": "Acme SL",
      "items": [{ "description": "Hora consultoria", "quantity": 1, "unitPrice": 95 }]
    },
    {
      "clientName": "TechStart SL",
      "items": [{ "description": "Desarrollo web", "quantity": 8, "unitPrice": 60 }]
    }
  ]
}
```

```bash
curl -X POST https://api.frihet.io/v1/invoices/batch \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "clientName": "Acme SL", "items": [{ "description": "Consultoria", "quantity": 1, "unitPrice": 95 }] },
      { "clientName": "TechStart SL", "items": [{ "description": "Desarrollo", "quantity": 8, "unitPrice": 60 }] }
    ]
  }'
```

**Respuesta (207 Multi-Status):**

```json
{
  "results": [
    { "status": 201, "data": { "id": "inv_001", "clientName": "Acme SL", "status": "draft" } },
    { "status": 201, "data": { "id": "inv_002", "clientName": "TechStart SL", "status": "draft" } }
  ],
  "summary": { "total": 2, "succeeded": 2, "failed": 0 }
}
```

Si alguno de los elementos falla la validacion, el resto se crea igualmente. Los errores individuales se devuelven en el array `results`:

```json
{
  "results": [
    { "status": 201, "data": { "id": "inv_001", "clientName": "Acme SL" } },
    { "status": 400, "error": { "message": "Missing required field: items" } }
  ],
  "summary": { "total": 2, "succeeded": 1, "failed": 1 }
}
```

**Limites:**

| Concepto           | Limite      |
| ------------------ | ----------- |
| Elementos por lote | 50 maximo   |
| Tamano de peticion | 1 MB maximo |

---

## Idempotencia

Las peticiones `POST` aceptan una cabecera `Idempotency-Key` para evitar la creacion duplicada de recursos ante reintentos de red. Si la misma clave se envia dentro de las siguientes **24 horas**, la API devuelve la respuesta original sin ejecutar la operacion de nuevo.

```bash
curl -X POST https://api.frihet.io/v1/invoices \
  -H "X-API-Key: fri_tu-clave-aqui" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "clientName": "Acme SL",
    "items": [{ "description": "Consultoria", "quantity": 10, "unitPrice": 95 }]
  }'
```

La clave se reserva contra la operacion **antes** de ejecutarla, no despues, y queda reservada 24 horas.

**Comportamiento:**

| Escenario                                                               | Resultado                                                                     |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Primera peticion con la clave                                           | Se crea el recurso normalmente                                                |
| Reintento de la **misma** operacion (dentro de 24h)                     | Se devuelve el **status y el cuerpo originales**, tal cual, sin ejecutar nada |
| La primera peticion sigue en curso, o su resultado no se pudo registrar | `409 IDEMPOTENCY_REQUEST_IN_PROGRESS`                                         |
| La misma clave en una operacion **distinta**                            | `409 IDEMPOTENCY_KEY_REUSED` — la segunda operacion **no** se ejecuta         |
| Misma clave despues de 24h                                              | Se trata como una peticion nueva                                              |

El status que se repite es el **almacenado**. Un `201` se repite como `201`, no como `200`. Esto incluye las respuestas de error: una peticion que respondio `4xx` o `5xx` repite ese mismo status y cuerpo en lugar de volver a ejecutarse.

**Cabecera de respuesta:**

Cuando la API detecta una clave repetida, incluye la cabecera `X-Idempotent-Replayed: true` para que el consumidor sepa que la respuesta es una replica.

```
HTTP/1.1 201 Created
X-Idempotent-Replayed: true
Content-Type: application/json
```

**Si un reintento recibe 409 `IDEMPOTENCY_REQUEST_IN_PROGRESS`:**

Cuando la primera peticion ya termino, ese 409 significa que su resultado **no se pudo registrar** — la respuesta era demasiado grande, no serializable, o la escritura de registro fallo. La API prefiere rechazar el reintento antes que ejecutar la operacion una segunda vez: en un documento fiscal, un duplicado no tiene vuelta atras.

**Reconcilia el estado; no reintentes sin mas con una clave nueva.** Consulta el recurso (`GET` de la coleccion, o busca el documento que la operacion habria creado) y decide a partir de su estado real. Una clave nueva crearia un segundo documento — exactamente lo que la primera clave existia para evitar.

**Identidad de la operacion:**

En `POST /v1/invoices/{invoiceId}/credit-note` el **cuerpo** forma parte de la identidad, asi que la misma clave con un cuerpo distinto devuelve `409 IDEMPOTENCY_KEY_REUSED`. En el resto de endpoints la identidad es el metodo y la ruta.

**Requisitos:**

- La clave debe tener como maximo **64 caracteres**
- Recomendamos usar UUID v4
- Solo aplica a peticiones `POST` (crear recursos)

---

## Busqueda

Los endpoints de listado soportan busqueda de texto completo mediante el parametro `q`:

```bash
curl "https://api.frihet.io/v1/invoices?q=acme" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

La busqueda se aplica sobre los campos de texto principales del recurso (nombre de cliente, descripcion, notas, etc.). Se puede combinar con los filtros existentes (`status`, `from`, `to`).

---

## Endpoints de inteligencia

Estos endpoints proporcionan datos agregados y contexto de negocio. Son especialmente utiles para agentes de IA y dashboards externos.

### Contexto de negocio (`/context`)

```
GET /v1/context
```

Devuelve un resumen completo del negocio, pensado para alimentar a agentes de IA con el contexto necesario para tomar decisiones informadas. Incluye resumen financiero, actividad reciente, alertas y configuracion fiscal.

```bash
curl https://api.frihet.io/v1/context \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "business": {
    "name": "BRTHLS Studio",
    "taxId": "12345678A",
    "fiscalZone": "canarias",
    "currency": "EUR"
  },
  "summary": {
    "revenue": { "invoiced": 15000, "paid": 12000, "pending": 2000, "overdue": 1000 },
    "expenses": { "total": 4500 },
    "profit": 7500,
    "counts": { "invoices": 25, "clients": 12, "products": 5 }
  },
  "recentActivity": [
    {
      "type": "invoice.created",
      "id": "inv_001",
      "description": "Factura para Acme SL",
      "timestamp": "2026-03-18T10:00:00Z"
    },
    {
      "type": "expense.created",
      "id": "exp_042",
      "description": "Adobe Creative Cloud",
      "timestamp": "2026-03-17T14:30:00Z"
    }
  ],
  "alerts": [
    { "type": "overdue", "count": 2, "amount": 1000 },
    { "type": "tax_deadline", "model": "303", "dueDate": "2026-04-20" }
  ]
}
```

### P&L mensual (`/monthly`)

```
GET /v1/monthly?month=YYYY-MM
```

Devuelve la cuenta de resultados (P&L) de un mes concreto: ingresos facturados, gastos por categoria, beneficio neto y comparativa con el mes anterior.

```bash
curl "https://api.frihet.io/v1/monthly?month=2026-02" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "month": "2026-02",
  "revenue": {
    "invoiced": 8500.0,
    "collected": 6200.0,
    "outstanding": 2300.0
  },
  "expenses": {
    "total": 3100.0,
    "byCategory": {
      "software": 450.0,
      "marketing": 800.0,
      "office": 350.0,
      "professional_services": 1500.0
    }
  },
  "profit": 5400.0,
  "comparison": {
    "revenueChange": 12.5,
    "expenseChange": -5.2,
    "profitChange": 22.1
  }
}
```

### Cifras fiscales trimestrales (`/quarterly`)

```
GET /v1/quarterly?quarter=YYYY-Q1
```

Endpoint **legacy**. Devuelve cifras agregadas del trimestre para el Modelo 303 (IVA) y el Modelo 130 (IRPF). Toda la respuesta vive bajo `data`; `meta` lleva `requestId` y `timestamp`.

El parametro `quarter` usa el formato `YYYY-Q[1-4]` (por defecto, el trimestre actual).

```bash
curl "https://api.frihet.io/v1/quarterly?quarter=2026-Q1" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": {
    "period": "2026-Q1",
    "months": ["2026-01", "2026-02", "2026-03"],
    "modelo303": {
      "baseImponible": 12000.0,
      "cuotaRepercutida": 2520.0,
      "baseDeducible": 4500.0,
      "cuotaDeducible": 980.0,
      "resultado": 1540.0
    },
    "modelo130": {
      "ingresos": 12000.0,
      "gastos": 4500.0,
      "rendimientoNeto": 7500.0,
      "pagoFraccionado": 1500.0
    },
    "summary": {
      "totalRevenue": 14520.0,
      "totalExpenses": 5480.0,
      "invoiceCount": 8,
      "clientCount": 5
    }
  },
  "meta": {
    "requestId": "req_a1b2c3",
    "timestamp": "2026-04-12T10:00:00Z"
  }
}
```

:::caution Divergencia: facturas borrador (`draft`)
`/quarterly` solo excluye facturas con estado `cancelled`, por lo que **incluye facturas en borrador (`draft`/proforma)** en los ingresos y en las cifras del 303/130. Esto puede **inflar** los importes frente a la liquidacion real.

Para cifras alineadas con AEAT (que excluyen tanto `cancelled` como `draft`), usa [`/v1/fiscal/modelo/{model}`](#modelos-fiscales-v1fiscalmodelo) — es la ruta autoritativa. Ademas, `/quarterly` no devuelve `model`, `readonly` ni la nota de AEAT, y su Modelo 303 es mas fino (sin `baseExenta`, `baseNoSujetaORC` ni `outOfScope`); el Modelo 130 de `/quarterly` no incluye `retencionesSoportadas`.
:::

:::tip
Los endpoints `/context`, `/monthly` y `/quarterly` estan disenados para ser un **punto de entrada para agentes de IA**. Proporcionan la informacion necesaria en una sola llamada, sin necesidad de consultar multiples endpoints individuales. Para datos fiscales code-truthful, prefiere `/v1/fiscal/modelo/{model}`.
:::

---

### Modelos fiscales (`/v1/fiscal/modelo`) {#modelos-fiscales-v1fiscalmodelo}

```
GET /v1/fiscal/modelo/{model}
```

Devuelve un resumen calculado de un modelo fiscal espanol a partir de las facturas y gastos ya almacenados. Es la ruta **autoritativa** para cifras fiscales: excluye tanto facturas `cancelled` como `draft` (proforma), alineandose con el criterio de AEAT.

:::warning No se presenta a AEAT
Este endpoint es de **solo lectura**. Calcula y agrega datos ya almacenados, pero **no presenta, firma ni transmite nada a AEAT** — ese es un acto fiscal irreversible que se mantiene fuera de la API a proposito. Cada respuesta incluye `readonly: true` y una `note` que lo recuerda.
:::

**Parametros de ruta:**

| Parametro | Valores             | Descripcion              |
| --------- | ------------------- | ------------------------ |
| `model`   | `303`, `130`, `390` | Modelo fiscal a calcular |

**Parametros de consulta:**

| Parametro | Aplica a     | Formato       | Descripcion                                           |
| --------- | ------------ | ------------- | ----------------------------------------------------- |
| `quarter` | `303`, `130` | `YYYY-Q[1-4]` | Trimestre. Por defecto, el trimestre actual           |
| `year`    | `390`        | `YYYY`        | Ano natural. Por defecto, el ano actual (suma Q1..Q4) |

:::note Campo `model`, no `modeloCode`
La respuesta identifica el modelo con el campo `model`, emitido como cadena literal (`"303"`, `"130"` o `"390"`). **No existe** un campo `modeloCode` ni un alias `model` para ningun otro nombre en esta respuesta.
:::

#### Modelo 303 (IVA trimestral)

```bash
curl "https://api.frihet.io/v1/fiscal/modelo/303?quarter=2026-Q1" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": {
    "model": "303",
    "period": "2026-Q1",
    "months": ["2026-01", "2026-02", "2026-03"],
    "modelo303": {
      "baseImponible": 12000.0,
      "cuotaRepercutida": 2520.0,
      "baseDeducible": 4500.0,
      "cuotaDeducible": 980.0,
      "resultado": 1540.0,
      "baseExenta": 0,
      "baseNoSujetaORC": 0,
      "outOfScope": {
        "igic": { "base": 0, "cuota": 0 },
        "ipsi": { "base": 0, "cuota": 0 }
      }
    },
    "summary": {
      "totalRevenue": 14520.0,
      "totalExpenses": 5480.0,
      "invoiceCount": 8,
      "expenseCount": 14,
      "clientCount": 5
    },
    "readonly": true,
    "note": "READ-ONLY summary. Not presented or submitted to AEAT."
  },
  "meta": {
    "requestId": "req_a1b2c3",
    "timestamp": "2026-04-12T10:00:00Z"
  }
}
```

**Campos de `modelo303`:**

| Campo              | Tipo   | Descripcion                                                                                                 |
| ------------------ | ------ | ----------------------------------------------------------------------------------------------------------- |
| `baseImponible`    | number | Base de operaciones interiores sujetas a IVA                                                                |
| `cuotaRepercutida` | number | IVA devengado (repercutido a clientes)                                                                      |
| `baseDeducible`    | number | Base de gastos con IVA deducible                                                                            |
| `cuotaDeducible`   | number | IVA soportado deducible                                                                                     |
| `resultado`        | number | `cuotaRepercutida − cuotaDeducible`. Positivo = a pagar; negativo = a devolver/compensar                    |
| `baseExenta`       | number | Base de operaciones exentas (se declara, cuota cero)                                                        |
| `baseNoSujetaORC`  | number | Base de operaciones intracomunitarias / exportacion / inversion del sujeto pasivo (ISP)                     |
| `outOfScope`       | object | Operaciones en otros impuestos: `igic` e `ipsi`, cada uno con `base` y `cuota`. **No** forman parte del 303 |

#### Modelo 130 (pago fraccionado IRPF)

```bash
curl "https://api.frihet.io/v1/fiscal/modelo/130?quarter=2026-Q1" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": {
    "model": "130",
    "period": "2026-Q1",
    "months": ["2026-01", "2026-02", "2026-03"],
    "modelo130": {
      "ingresos": 12000.0,
      "gastos": 4500.0,
      "rendimientoNeto": 7500.0,
      "pagoFraccionado": 1500.0,
      "retencionesSoportadas": 0
    },
    "summary": {
      "totalRevenue": 14520.0,
      "totalExpenses": 5480.0,
      "invoiceCount": 8,
      "expenseCount": 14,
      "clientCount": 5
    },
    "readonly": true,
    "note": "READ-ONLY summary. Not presented or submitted to AEAT."
  },
  "meta": {
    "requestId": "req_a1b2c3",
    "timestamp": "2026-04-12T10:00:00Z"
  }
}
```

**Campos de `modelo130`:**

| Campo                   | Tipo   | Descripcion                                                                       |
| ----------------------- | ------ | --------------------------------------------------------------------------------- |
| `ingresos`              | number | Ingresos del periodo                                                              |
| `gastos`                | number | Gastos del periodo                                                                |
| `rendimientoNeto`       | number | Rendimiento neto (`ingresos − gastos`)                                            |
| `pagoFraccionado`       | number | Pago fraccionado IRPF (20% del rendimiento neto, estimacion directa simplificada) |
| `retencionesSoportadas` | number | IRPF retenido por los clientes en las facturas emitidas                           |

:::note `retencionesSoportadas` solo en `/fiscal/modelo`
El campo `retencionesSoportadas` se devuelve aqui, en `/v1/fiscal/modelo/130`, pero **no** en el endpoint legacy `/v1/quarterly`.
:::

#### Modelo 390 (resumen anual de IVA)

```bash
curl "https://api.frihet.io/v1/fiscal/modelo/390?year=2026" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "data": {
    "model": "390",
    "period": "2026",
    "months": ["2026-01", "2026-02", "..."],
    "modelo390": {
      "baseImponible": 48000.0,
      "cuotaRepercutida": 10080.0,
      "baseDeducible": 18000.0,
      "cuotaDeducible": 3920.0,
      "resultadoAnual": 6160.0,
      "baseExenta": 0,
      "baseNoSujetaORC": 0,
      "outOfScope": {
        "igic": { "base": 0, "cuota": 0 },
        "ipsi": { "base": 0, "cuota": 0 }
      }
    },
    "summary": {
      "totalRevenue": 58080.0,
      "totalExpenses": 21920.0,
      "invoiceCount": 32,
      "expenseCount": 56,
      "clientCount": 11
    },
    "readonly": true,
    "note": "READ-ONLY annual VAT recap. Not presented or submitted to AEAT."
  },
  "meta": {
    "requestId": "req_a1b2c3",
    "timestamp": "2026-04-12T10:00:00Z"
  }
}
```

:::note `resultadoAnual`, no `resultado`
El Modelo 390 es una proyeccion anual del mismo motor del 303 (suma de Q1..Q4). El resultado neto se llama `resultadoAnual` (no `resultado`).
:::

**Codigos de respuesta:**

| Codigo | Cuando ocurre                                                                      |
| ------ | ---------------------------------------------------------------------------------- |
| `200`  | Resumen calculado correctamente                                                    |
| `400`  | Formato de `quarter` o `year` invalido. Usa `YYYY-Q[1-4]` (303/130) o `YYYY` (390) |
| `404`  | Modelo desconocido. Usa `303`, `130` o `390`                                       |

Las respuestas de error tambien incluyen el bloque `meta` con `requestId` y `timestamp`.

---

## Dashboard financiero (`/summary`)

```
GET /v1/summary
```

Devuelve un resumen financiero del negocio: ingresos, gastos, beneficio y contadores.

**Parametros de consulta:**

| Parametro | Tipo   | Descripcion             |
| --------- | ------ | ----------------------- |
| `from`    | string | Fecha inicio (ISO 8601) |
| `to`      | string | Fecha fin (ISO 8601)    |

```bash
curl "https://api.frihet.io/v1/summary?from=2026-01-01&to=2026-03-31" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

**Respuesta (200):**

```json
{
  "period": { "from": "2026-01-01", "to": "2026-03-31" },
  "revenue": {
    "invoiced": 15000.0,
    "paid": 12000.0,
    "pending": 2000.0,
    "overdue": 1000.0
  },
  "expenses": { "total": 4500.0 },
  "profit": 7500.0,
  "counts": {
    "invoices": 25,
    "quotes": 8,
    "expenses": 42,
    "clients": 12,
    "products": 5
  },
  "invoicesByStatus": {
    "draft": 3,
    "sent": 5,
    "paid": 15,
    "overdue": 2
  },
  "overdue": { "count": 2, "amount": 1000.0 }
}
```

---

## Codigos de error

La API utiliza codigos HTTP estandar. Las respuestas de error incluyen un objeto JSON con los campos `error` y, opcionalmente, `message` y `details`.

| Codigo | Significado           | Descripcion                                                                            |
| ------ | --------------------- | -------------------------------------------------------------------------------------- |
| `400`  | Bad Request           | Falta un campo requerido, formato incorrecto o campo no permitido                      |
| `401`  | Unauthorized          | La API key no se ha proporcionado, es invalida, tiene formato incorrecto o ha expirado |
| `403`  | Forbidden             | La API key no tiene permisos para acceder a este recurso                               |
| `404`  | Not Found             | El recurso solicitado no existe                                                        |
| `405`  | Method Not Allowed    | El metodo HTTP no esta soportado para este endpoint                                    |
| `413`  | Payload Too Large     | El cuerpo de la peticion excede 1 MB                                                   |
| `422`  | Unprocessable Entity  | Datos validos pero el servidor no puede procesarlos (ej: perfil fiscal no configurado) |
| `429`  | Too Many Requests     | Se ha excedido el limite de 100 peticiones por minuto                                  |
| `500`  | Internal Server Error | Error interno del servidor                                                             |

### Estructura de las respuestas de error

**Error de validacion (400):**

```json
{
  "error": "Validation error",
  "details": [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "undefined",
      "path": ["clientName"],
      "message": "Required"
    }
  ]
}
```

Los errores de validacion usan el formato de Zod. El campo `path` indica que campo contiene el error y `message` describe el problema.

**Clave invalida o expirada (401):**

```json
{
  "error": "Invalid or expired API key"
}
```

**Formato de clave invalido (401):**

```json
{
  "error": "Invalid API key format"
}
```

**Recurso no encontrado (404):**

```json
{
  "error": "Resource not found"
}
```

**Estado invalido en filtro (400):**

```json
{
  "error": "Invalid status filter",
  "message": "Valid values: draft, sent, paid, overdue, cancelled"
}
```

**Rate limit excedido (429):**

```json
{
  "error": "Rate limit exceeded",
  "message": "Maximum 100 requests per minute",
  "retryAfter": 60
}
```

**Error interno (500):**

```json
{
  "error": "Internal server error"
}
```

---

## Paginacion

Los endpoints de listado devuelven resultados paginados con la siguiente estructura:

```json
{
  "data": [],
  "total": 142,
  "limit": 50,
  "offset": 0
}
```

- `total`: numero total de registros que cumplen los filtros aplicados
- `limit`: numero de registros devueltos en esta pagina (maximo 100)
- `offset`: numero de registros saltados (maximo 10.000)

Para obtener la siguiente pagina:

```bash
curl "https://api.frihet.io/v1/invoices?limit=50&offset=50" \
  -H "X-API-Key: fri_tu-clave-aqui"
```

Los resultados se ordenan por la fecha natural del recurso en orden descendente (mas recientes primero):

- Facturas y presupuestos: `issueDate`
- Gastos: `date`
- Clientes y productos: `createdAt`

---

## Validacion estricta

La API usa **validacion estricta** (Zod strict mode). Las peticiones con campos desconocidos se rechazan con un error `400`:

```json
{
  "error": "Validation error",
  "details": [
    {
      "code": "unrecognized_keys",
      "keys": ["campoInventado"],
      "path": [],
      "message": "Unrecognized key(s) in object: 'campoInventado'"
    }
  ]
}
```

Esto previene errores silenciosos por typos en los nombres de los campos.

---

## CORS

La API soporta CORS para peticiones desde el navegador. Los origenes permitidos son:

- `https://app.frihet.io`
- `https://frihet.io`
- `https://www.frihet.io`

Para integraciones server-to-server, CORS no es relevante. Si necesitas acceder a la API desde un dominio diferente en el navegador, utiliza un proxy en tu backend.

---

## Cabeceras de seguridad

Todas las respuestas incluyen cabeceras de seguridad:

| Cabecera                 | Valor                                          |
| ------------------------ | ---------------------------------------------- |
| `X-Content-Type-Options` | `nosniff`                                      |
| `X-Frame-Options`        | `DENY`                                         |
| `X-XSS-Protection`       | `1; mode=block`                                |
| `X-API-Version`          | Version de la API (`2026-03-18`)               |
| `X-Request-Id`           | ID unico de la peticion (util para depuracion) |

---

## OAuth para MCP

El endpoint `POST /api/oauth/api-key` permite provisionar una API key automaticamente a traves del flujo OAuth de MCP. El servidor MCP usa este endpoint internamente -- no necesitas llamarlo directamente.

El flujo:

1. El usuario se autentica en la app de Frihet
2. El cliente MCP envia el token de sesion a `/api/oauth/api-key`
3. El servidor devuelve una nueva clave `fri_xxx...` etiquetada como "MCP OAuth"
4. La clave expira a los 365 dias

Limite: 5 claves activas por usuario. Si se supera el limite, el endpoint responde con un `429`.

---

## Buenas practicas

1. **Almacena la API key de forma segura.** Nunca la incluyas en codigo frontend, repositorios publicos o logs.
2. **Gestiona el rate limiting.** Implementa backoff exponencial si recibes un `429`.
3. **Usa paginacion.** No pidas todos los registros de golpe; itera con `limit` y `offset`.
4. **Verifica los codigos de respuesta.** No asumas que todas las peticiones seran exitosas.
5. **Rota las claves periodicamente.** Crea una nueva clave, actualiza tu integracion y revoca la anterior.
6. **Usa HTTPS siempre.** Todas las peticiones a la API deben ser sobre HTTPS.
7. **Usa filtros.** Los parametros `status`, `from` y `to` reducen la cantidad de datos transferidos.
8. **Aprovecha PATCH.** Para actualizaciones parciales, envia solo los campos modificados.
