API REST
La API REST de Frihet permite acceder y manipular los recursos de tu cuenta de forma programática. Toda la comunicación se realiza sobre HTTPS y las respuestas son JSON.
URL base
https://api.frihet.io/v1
Todos los endpoints descritos en esta página son relativos a esta URL base.
Descubrimiento: Una petición GET a la raíz (https://api.frihet.io/) devuelve los enlaces principales sin necesidad de autenticación:
{
"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 especificación OpenAPI 3.1 esta disponible en https://api.frihet.io/openapi.yaml.
Si usas TypeScript o JavaScript, el SDK oficial simplifica la integración:
npm install @frihet/sdk
import Frihet from '@frihet/sdk';
const frihet = new Frihet({ apiKey: 'fri_...' });
const invoices = await frihet.invoices.list({ status: 'overdue' });
Repositorio: github.com/Frihet-io/frihet-sdk
Autenticación
Cada petición debe incluir una API key en la cabecera X-API-Key. Las claves se crean desde Integraciones → API Keys en el panel de Frihet.
Las claves tienen el prefijo fri_ seguido de 32 bytes aleatorios codificados en base64url. Ejemplo: fri_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345678.
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:
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 creación. No se puede recuperar después.
- 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 expiración configurable.
- Si sospechas que una clave ha sido comprometida, revócala inmediatamente desde el panel.
Ciclo de vida de las claves
Crear una clave:
- Ve a Integraciones → API Keys
- Pulsa Crear clave
- Asigna un nombre descriptivo (por ejemplo,
integracion-contabilidad) - Opcionalmente, establece una fecha de expiración en días. Si lo dejas vacío, la clave no caduca
- Copia la clave inmediatamente -- no podrás verla de nuevo
Expiración:
Las claves con fecha de expiración dejan de funcionar automáticamente al cumplirse el plazo. Las peticiones con una clave expirada reciben un 401 Unauthorized.
Revocación:
Puedes revocar una clave en cualquier momento desde Integraciones → API Keys. La revocación es inmediata e irreversible: las peticiones en curso con esa clave fallaran a partir de ese instante.
Rotación de claves:
Para rotar una clave sin interrumpir el servicio:
- Crea una nueva clave con el mismo alcance
- Actualiza tu integración para usar la nueva clave
- Verifica que las peticiones funcionan correctamente
- Revoca la clave anterior
Monitorización:
Cada clave muestra la fecha de último uso en el panel de Ajustes. Revisa periódicamente las claves inactivas y revoca las que ya no se utilicen.
Rate limiting
Cada clave API tiene un límite de 100 peticiones por minuto. Si se excede, la API responde con un código 429:
{
"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 límite de forma proactiva:
| Cabecera | Descripción | 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
Gestión del código 429:
Cuando recibes un 429, usa las cabeceras para calcular cuanto esperar antes de reintentar:
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 enX-RateLimit-Resetantes de reintentar. - Monitoriza
X-RateLimit-Remainingpara frenar las peticiones antes de alcanzar el límite. - Distribuye las peticiones en el tiempo en lugar de enviar ráfagas.
Tamaño de petición
El cuerpo de las peticiones POST, PUT y PATCH no puede exceder 1 MB. Peticiones mayores reciben un código 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). Además, los clientes tienen subcolecciones CRM: contacts, activities y notes.
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
Parámetros de consulta:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit | integer | 50 | Resultados por página (máximo 100) |
offset | integer | 0 | Número de resultados a saltar (máximo 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 numeración |
q | string | -- | Búsqueda de texto (nombre de cliente, número, notas) |
cursor | string | -- | Cursor para paginación basada en cursor (ignora offset) |
fields | string | -- | Campos a devolver, separados por coma (siempre incluye id) |
Ejemplo:
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):
{
"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
curl https://api.frihet.io/v1/invoices/abc123 \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"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 | Descripción |
|---|---|---|
clientName | string | Nombre del cliente (max 10.000 caracteres) |
items | array | Lista de líneas de la factura. Cada línea: { description, quantity, unitPrice } |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
status | string | draft (defecto), sent, partial, paid, overdue, cancelled |
issueDate | string | Fecha de emisión (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 línea (items[]):
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
description | string | Si | Descripción del concepto (max 10.000 caracteres) |
quantity | number | Si | Cantidad |
unitPrice | number | Si | Precio unitario |
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 web"
}'
Respuesta (201):
{
"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 web",
"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.
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.
Si envías items, debes enviar el array completo -- no se admiten actualizaciones parciales de líneas individuales.
Eliminar factura
DELETE /v1/invoices/:id
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.
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
Envía la factura por correo electrónico al destinatario especificado. Si la factura esta en estado draft, se actualiza automáticamente a sent.
Campos:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
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 |
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@example.com",
"recipientName": "Departamento de Contabilidad",
"locale": "es"
}'
Respuesta (200):
{ "success": true, "messageId": "re_abc123..." }
Marcar factura como pagada
POST /v1/invoices/:id/paid
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
paidDate | string | No | Fecha de pago (ISO 8601). Por defecto: hoy |
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):
{ "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 crédito a partir de una factura existente. El endpoint crea, no emite: no asigna número fiscal, no calcula hash y no envía a VeriFactu. Esos pasos ocurren cuando una persona emite el borrador desde la aplicación.
Requisitos:
- Plan
proo superior. Si no:403 PLAN_UPGRADE_REQUIRED. - Cabecera
Idempotency-Keyobligatoria (a diferencia del resto dePOST, donde es opcional). Si falta:400 IDEMPOTENCY_KEY_REQUIRED. - La factura original no puede ser
draft, ni estarcancelled, 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 | Descripción |
|---|---|---|---|
reason | string | Si | Motivo: refund, discount, error, cancellation, other |
reasonDescription | string | No | Descripción libre del motivo (max. 500 caracteres) |
fullCredit | boolean | No | Solo true (o se omite). Por defecto: true |
issueDate | string | No | Fecha de emisión (YYYY-MM-DD). Por defecto: hoy |
Los abonos parciales no están disponibles en la API: fullCredit: false devuelve 400 PARTIAL_CREDIT_NOT_IMPLEMENTED. Una rectificación parcial necesita las líneas corregidas, y este endpoint no las acepta; hazla en la aplicación.
La rectificación es siempre por diferencias (rectificationMethod: "I"). El método S (por sustitución) no se ofrece en la API: exigiría reexpresar también los importes finales corregidos, que este endpoint no puede expresar.
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):
{
"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, más la cabecera X-Idempotent-Replayed: true. No se crea un segundo borrador. Ver Idempotencia.
Tipo R: el tipo de rectificativa no se envía, se deriva del reason. La API no acepta un campo type.
Motivo reason | Tipo R asignado | Descripción legal |
|---|---|---|
error | R1 | Art. 80.1, 80.2 y 80.6 LIVA; art. 15 del Real Decreto 1619/2012 |
refund | R4 | Resto de causas (devolución, descuento, anulación) |
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, créditos 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 aplicación.
Descargar factura en XML (EN16931)
GET /v1/invoices/:id/xml
Devuelve el fichero de e-factura EN16931 (UBL 2.1 o CII, Cross Industry Invoice) que Frihet generó para la factura.
Auth: API key requerida (X-API-Key).
Respuesta: application/xml con cabecera Content-Disposition: attachment.
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 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:
| Código | Descripción |
|---|---|
404 | Factura no encontrada, o su e-factura todavía no se ha generado |
Elegir el formato del XML desde Ajustes: No disponible en Frihet. El endpoint devuelve el fichero que Frihet generó al guardar o enviar la factura; si todavía no existe, responde 404.
Aplicar recargo por demora
POST /v1/invoices/:id/late-fee
Aplica un recargo por demora a una factura vencida. Genera una nota de débito con el importe calculado según la configuración de recargos de la cuenta (Ajustes → Recargos por mora).
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: {} (vacío). Los parámetros del recargo se toman de la configuración de la cuenta.
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):
{
"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:
| Código | Descripción |
|---|---|
404 | Factura no encontrada |
422 | La factura no esta en estado overdue o ya tiene un recargo por demora activo |
Define el método (porcentaje anual, importe fijo o tipo legal de demora) en Ajustes → Recargos por mora. El cálculo usa la Ley 3/2004 (LMOC) para operaciones entre empresas por defecto.
Gastos (/expenses)
Listar gastos
GET /v1/expenses
Parámetros de consulta:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit | integer | 50 | Resultados por página (máximo 100) |
offset | integer | 0 | Número 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 categoría |
vendorId | string | -- | Filtrar por ID de proveedor |
q | string | -- | Búsqueda de texto (proveedor, descripción, número) |
cursor | string | -- | Cursor para paginación basada en cursor |
fields | string | -- | Campos a devolver, separados por coma |
curl "https://api.frihet.io/v1/expenses?limit=20&from=2026-01-01" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"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 | Descripción |
|---|---|---|
description | string | Descripción del gasto (max 10.000 caracteres) |
amount | number | Importe del gasto |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
category | string | Categoría 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 |
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):
{
"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
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 | Descripción |
|---|---|---|---|
clientId | string | Si | ID del cliente al que facturar |
markup | number | No | Porcentaje de markup (0-1000%) |
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
Parámetros de consulta:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit | integer | 50 | Resultados por página (máximo 100) |
offset | integer | 0 | Número 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 | -- | Búsqueda de texto (nombre, email, NIF) |
cursor | string | -- | Cursor para paginación basada en cursor |
fields | string | -- | Campos a devolver, separados por coma |
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 | Descripción |
|---|---|---|
name | string | Nombre del cliente (max 10.000 caracteres) |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
email | string | Email de contacto |
phone | string | Teléfono |
taxId | string | NIF/CIF/VAT |
address | object | Dirección (ver estructura abajo) |
Estructura de address:
| Campo | Tipo | Descripción |
|---|---|---|
street | string | Calle y número |
city | string | Ciudad |
state | string | Provincia o estado |
postalCode | string | Código postal |
country | string | País |
Todos los campos de address son opcionales.
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@example.com",
"taxId": "B12345674",
"address": {
"street": "Calle Gran Via 42",
"city": "Madrid",
"postalCode": "28013",
"country": "ES"
}
}'
Respuesta (201):
{
"id": "cli001",
"name": "Acme S.L.",
"email": "admin@example.com",
"taxId": "B12345674",
"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
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 válido en la URL.
Personas de contacto (/v1/clients/:id/contacts)
Listar contactos
GET /v1/clients/:id/contacts
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
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 | Descripción |
|---|---|---|
name | string | Nombre de la persona de contacto |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
email | string | Email del contacto |
phone | string | Teléfono |
role | string | Cargo o rol (ej. "Director financiero") |
isPrimary | boolean | Si es el contacto principal del cliente |
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@example.com",
"phone": "+34 612 345 678",
"role": "Directora financiera",
"isPrimary": true
}'
Respuesta (201):
{
"id": "con001",
"name": "Maria Garcia",
"email": "maria@example.com",
"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
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
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 automáticamente. También puedes crear actividades manuales.
Las actividades son inmutables. No se pueden actualizar ni eliminar una vez creadas.
Listar actividades
GET /v1/clients/:id/activities
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
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 | Descripción |
|---|---|---|
type | string | Tipo de actividad: call, email, meeting o task |
title | string | Titulo descriptivo de la actividad |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
description | string | Descripción detallada |
metadata | object | Datos adicionales en formato libre |
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 automáticamente y no se pueden crear vía API.
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):
{
"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
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
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 | Descripción |
|---|---|---|
content | string | Contenido de la nota |
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):
{
"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
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
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 | Descripción |
|---|---|---|
name | string | Nombre del producto o servicio (max 10.000 caracteres) |
unitPrice | number | Precio unitario |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
description | string | Descripción (max 10.000 caracteres) |
taxRate | number | Porcentaje de impuesto (0-100) |
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):
{
"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
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
Parámetros de consulta:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit | integer | 50 | Resultados por página (máximo 100) |
offset | integer | 0 | Número 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 |
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 | Descripción |
|---|---|---|
clientName | string | Nombre del cliente (max 10.000 caracteres) |
items | array | Líneas del presupuesto. Cada línea: { description, quantity, unitPrice } |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
validUntil | string | Fecha de validez (ISO 8601) |
notes | string | Notas o condiciones (max 10.000 caracteres) |
status | string | draft (defecto), sent, accepted, rejected, expired |
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):
{
"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.
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 automáticamente 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 | Descripción |
|---|---|---|
name | string | Nombre del proveedor |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
email | string | Email de contacto |
phone | string | Teléfono |
taxId | string | NIF/CIF/VAT |
address | object | Dirección (street, city, province, zip, country, countryCode) |
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
Depósitos (/deposits)
Los depósitos permiten registrar pagos anticipados o señales de clientes antes de emitir la factura final. Un depósito puede aplicarse total o parcialmente a una o varias facturas, o reembolsarse al cliente.
Listar depósitos
GET /v1/deposits
Parámetros de consulta:
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
limit | integer | 50 | Resultados por página (máximo 100) |
offset | integer | 0 | Número 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 paginación basada en cursor (ignora offset) |
fields | string | -- | Campos a devolver, separados por coma (siempre incluye id) |
Ejemplo:
curl "https://api.frihet.io/v1/deposits?clientId=cli001&status=pending" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"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 depósito
GET /v1/deposits/{id}
curl https://api.frihet.io/v1/deposits/dep001 \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"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 depósito
POST /v1/deposits
Campos requeridos:
| Campo | Tipo | Descripción |
|---|---|---|
clientId | string | ID del cliente |
amount | number | Importe del depósito (debe ser positivo) |
Campos opcionales:
| Campo | Tipo | Descripción |
|---|---|---|
currency | string | Código ISO 4217 (3 caracteres). Por defecto: EUR |
date | string | Fecha del depósito (ISO 8601: YYYY-MM-DD). Por defecto: hoy |
description | string | Descripción o concepto del depósito (max 10.000 caracteres) |
paymentMethod | string | Método de pago: bank_transfer, card, cash, other |
reference | string | Referencia del pago (max 500 caracteres) |
notes | string | Notas internas (max 10.000 caracteres) |
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):
{
"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 depósito (PATCH)
PATCH /v1/deposits/{id}
Solo necesitas enviar los campos que quieras modificar. Los campos no incluidos permanecen sin cambios.
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 depósito actualizado.
Eliminar depósito
DELETE /v1/deposits/{id}
curl -X DELETE https://api.frihet.io/v1/deposits/dep001 \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta: 204 No Content
Solo se pueden eliminar depósitos en estado pending. Si el depósito ya ha sido aplicado parcial o totalmente, la API devuelve 422.
Aplicar depósito a una factura
POST /v1/deposits/{id}/apply
Descuenta el depósito (total o parcial) del importe de una factura. Si el importe aplicado agota el saldo del depósito, el estado cambia a applied. Si queda saldo restante, el estado pasa a partial.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
invoiceId | string | Si | ID de la factura a la que aplicar el depósito |
amount | number | Si | Importe a aplicar. No puede exceder remainingAmount |
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):
{
"success": true,
"deposit": {
"id": "dep001",
"status": "partial",
"appliedAmount": 1500,
"remainingAmount": 500
},
"invoice": {
"id": "abc123",
"status": "partial",
"amountDue": 800
}
}
Errores posibles:
| Código | Descripción |
|---|---|
404 | Depósito o factura no encontrados |
422 | El importe supera el saldo disponible, o la factura ya esta pagada o cancelada |
Reembolsar depósito
POST /v1/deposits/{id}/refund
Registra la devolución total o parcial del depósito al cliente. El estado del depósito cambia a refunded.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
amount | number | No | Importe a reembolsar. Si se omite, se reembolsa el remainingAmount completo |
reason | string | No | Motivo del reembolso (max 1.000 caracteres) |
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):
{
"success": true,
"deposit": {
"id": "dep001",
"status": "refunded",
"appliedAmount": 1500,
"remainingAmount": 0,
"refundedAmount": 500
}
}
Errores posibles:
| Código | Descripción |
|---|---|
404 | Depósito no encontrado |
422 | El importe supera el saldo disponible o el depósito ya ha sido reembolsado completamente |
Operaciones por lote (/batch)
Todos los recursos principales soportan creación en lote. Envía un array de hasta 50 elementos en una sola petición.
POST /v1/{resource}/batch
Recursos soportados: invoices, expenses, clients, vendors, products, quotes, deposits
Cuerpo de la petición:
{
"items": [
{
"clientName": "Acme SL",
"items": [{ "description": "Hora consultoria", "quantity": 1, "unitPrice": 95 }]
},
{
"clientName": "TechStart SL",
"items": [{ "description": "Desarrollo web", "quantity": 8, "unitPrice": 60 }]
}
]
}
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):
{
"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 validación, el resto se crea igualmente. Los errores individuales se devuelven en el array results:
{
"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 }
}
Límites:
| Concepto | Límite |
|---|---|
| Elementos por lote | 50 máximo |
| Tamaño de petición | 1 MB máximo |
Idempotencia
Las peticiones POST aceptan una cabecera Idempotency-Key para evitar la creación duplicada de recursos ante reintentos de red. Si la misma clave se envía dentro de las siguientes 24 horas, la API devuelve la respuesta original sin ejecutar la operación de nuevo.
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 operación antes de ejecutarla, no después, y queda reservada 24 horas.
Comportamiento:
| Escenario | Resultado |
|---|---|
| Primera petición con la clave | Se crea el recurso normalmente |
| Reintento de la misma operación (dentro de 24h) | Se devuelve el status y el cuerpo originales, tal cual, sin ejecutar nada |
| La primera petición sigue en curso, o su resultado no se pudo registrar | 409 IDEMPOTENCY_REQUEST_IN_PROGRESS |
| La misma clave en una operación distinta | 409 IDEMPOTENCY_KEY_REUSED — la segunda operación no se ejecuta |
| Misma clave después de 24h | Se trata como una petición 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 petición que respondió 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 petición 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 operación una segunda vez: en un documento fiscal, un duplicado no tiene vuelta atrás.
Reconcilia el estado; no reintentes sin más con una clave nueva. Consulta el recurso (GET de la colección, o busca el documento que la operación habría creado) y decide a partir de su estado real. Una clave nueva crearía un segundo documento — exactamente lo que la primera clave existía para evitar.
Identidad de la operación:
En POST /v1/invoices/{invoiceId}/credit-note el cuerpo forma parte de la identidad, así que la misma clave con un cuerpo distinto devuelve 409 IDEMPOTENCY_KEY_REUSED. En el resto de endpoints la identidad es el método y la ruta.
Requisitos:
- La clave debe tener como máximo 64 caracteres
- Recomendamos usar UUID v4
- Solo aplica a peticiones
POST(crear recursos)
Búsqueda
Los endpoints de listado soportan búsqueda de texto completo mediante el parámetro q:
curl "https://api.frihet.io/v1/invoices?q=acme" \
-H "X-API-Key: fri_tu-clave-aqui"
La búsqueda se aplica sobre los campos de texto principales del recurso (nombre de cliente, descripción, 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 útiles 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 configuración fiscal.
curl https://api.frihet.io/v1/context \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"business": {
"name": "BRTHLS Studio",
"taxId": "12345678Z",
"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 categoría, beneficio neto y comparativa con el mes anterior.
curl "https://api.frihet.io/v1/monthly?month=2026-02" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"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 parámetro quarter usa el formato YYYY-Q[1-4] (por defecto, el trimestre actual).
curl "https://api.frihet.io/v1/quarterly?quarter=2026-Q1" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"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,
"outOfScope": {
"igic": {
"base": 0,
"cuota": 0
},
"ipsi": {
"base": 0,
"cuota": 0
}
}
},
"modelo130": {
"ingresos": 12000.0,
"gastos": 4500.0,
"rendimientoNeto": 7500.0,
"pagoFraccionado": 1500.0,
"retencionesSoportadas": 0,
"basis": "ytd",
"year": 2026,
"quarter": 1,
"porcentaje": 20,
"casillas": {
"c01": 12000.0,
"c02": 4500.0,
"c03": 7500.0,
"c04": 1500.0,
"c05": 0,
"c06": 0,
"c07": 1500.0,
"c12": 1500.0,
"c13": null,
"c14": null,
"c15": null,
"c16": null,
"c17": null,
"c18": null,
"c19": null
},
"pagosAnterioresPorTrimestre": [],
"excluded": {
"invoicesWithoutValidDate": 0,
"expensesWithoutValidDate": 0
},
"notModelled": ["c18: autoliquidación complementaria"],
"sourceUrl": "https://sede.agenciatributaria.gob.es/Sede/impuestos-tasas/impuesto-sobre-renta-personas-fisicas/modelo-130-irpf______esionales-estimacion-directa-fraccionado_/instrucciones.html"
},
"summary": {
"totalRevenue": 14520.0,
"totalExpenses": 5480.0,
"invoiceCount": 8,
"clientCount": 5
}
},
"meta": {
"requestId": "req_a1b2c3",
"timestamp": "2026-04-12T10:00:00Z"
}
}
El bloque modelo130 lleva las casillas del Modelo 130 acumuladas desde el 1 de enero hasta el final del trimestre (basis: "ytd"). En el ejemplo, notModelled está abreviado: la respuesta trae la lista completa de lo que el cálculo no incluye.
/v1/fiscal/modelo/{model}/quarterly excluye las facturas cancelled y draft, igual que /v1/fiscal/modelo/{model}. Su Modelo 303 es más resumido: no devuelve baseExenta ni baseNoSujetaORC, y las cuotas de IGIC e IPSI van aparte, en outOfScope. Para el detalle del modelo, usa /v1/fiscal/modelo/{model}.
Los endpoints /context, /monthly y /quarterly están diseñados para ser un punto de entrada para agentes de IA. Proporcionan la información necesaria en una sola llamada, sin necesidad de consultar múltiples endpoints individuales. Para datos fiscales code-truthful, prefiere /v1/fiscal/modelo/{model}.
Modelos fiscales (/v1/fiscal/modelo)
GET /v1/fiscal/modelo/{model}
Devuelve un resumen calculado de un modelo fiscal español a partir de las facturas y gastos ya almacenados. Es la ruta autoritativa para cifras fiscales: excluye tanto facturas cancelled como draft (proforma), alineándose con el criterio de 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 propósito. Cada respuesta incluye readonly: true y una note que lo recuerda.
Parámetros de ruta:
| Parámetro | Valores | Descripción |
|---|---|---|
model | 303, 130, 390, 347 | Modelo fiscal a calcular |
Parámetros de consulta:
| Parámetro | Aplica a | Formato | Descripción |
|---|---|---|---|
quarter | 303, 130 | YYYY-Q[1-4] | Trimestre. Por defecto, el trimestre actual |
year | 390, 347 | YYYY | Año natural. Por defecto, el año actual (en el 390, suma de Q1 a Q4) |
modeloCode y modelLa respuesta identifica el modelo con dos campos de igual valor, modeloCode y model, emitidos como cadena ("303", "130", "390" o "347").
Modelo 303 (IVA trimestral)
curl "https://api.frihet.io/v1/fiscal/modelo/303?quarter=2026-Q1" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"data": {
"modeloCode": "303",
"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 | Descripción |
|---|---|---|
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 / exportación / inversión 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)
curl "https://api.frihet.io/v1/fiscal/modelo/130?quarter=2026-Q1" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"data": {
"modeloCode": "130",
"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,
"basis": "ytd",
"year": 2026,
"quarter": 1,
"porcentaje": 20,
"casillas": {
"c01": 12000.0,
"c02": 4500.0,
"c03": 7500.0,
"c04": 1500.0,
"c05": 0,
"c06": 0,
"c07": 1500.0,
"c12": 1500.0,
"c13": null,
"c14": null,
"c15": null,
"c16": null,
"c17": null,
"c18": null,
"c19": null
},
"pagosAnterioresPorTrimestre": [],
"excluded": {
"invoicesWithoutValidDate": 0,
"expensesWithoutValidDate": 0
},
"notModelled": ["c18: autoliquidación complementaria"],
"sourceUrl": "https://sede.agenciatributaria.gob.es/Sede/impuestos-tasas/impuesto-sobre-renta-personas-fisicas/modelo-130-irpf______esionales-estimacion-directa-fraccionado_/instrucciones.html"
},
"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. Modelo 130 casillas are year-to-date; c13-c19 are not computed (see modelo130.notModelled)."
},
"meta": {
"requestId": "req_a1b2c3",
"timestamp": "2026-04-12T10:00:00Z"
}
}
El bloque modelo130 lleva las casillas del Modelo 130 acumuladas desde el 1 de enero hasta el final del trimestre (basis: "ytd"); period, months y summary se refieren solo al trimestre. En el ejemplo, notModelled está abreviado: la respuesta trae la lista completa de lo que el cálculo no incluye.
Campos de modelo130:
| Campo | Tipo | Descripción |
|---|---|---|
ingresos | number | Ingresos acumulados desde el 1 de enero (casilla 01) |
gastos | number | Gastos deducibles acumulados desde el 1 de enero (casilla 02) |
rendimientoNeto | number | ingresos − gastos (casilla 03) |
pagoFraccionado | number | Casilla 12: c04 − c05 − c06 (casilla 07), o 0 si sale negativa. Para estimación directa, normal o simplificada |
retencionesSoportadas | number | IRPF retenido por los clientes en las facturas emitidas, acumulado desde el 1 de enero (casilla 06) |
basis | string | Siempre "ytd": casillas acumuladas desde el 1 de enero |
year | number | Ejercicio |
quarter | number | Trimestre (1 a 4) |
porcentaje | number | Porcentaje que la casilla 04 aplica al rendimiento neto positivo (20) |
casillas | object | Casillas c01 a c07 y c12. De c13 a c19 van a null |
pagosAnterioresPorTrimestre | array | Casilla 07 de cada trimestre anterior del año: quarter y c07 |
excluded | object | Número de facturas y de gastos sin fecha válida, que no entran en el cálculo |
notModelled | array | Lo que el cálculo no incluye |
sourceUrl | string | Instrucciones del Modelo 130 en la sede electrónica de la AEAT |
Modelo 390 (resumen anual de IVA)
curl "https://api.frihet.io/v1/fiscal/modelo/390?year=2026" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"data": {
"modeloCode": "390",
"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"
}
}
resultadoAnual, no resultadoEl Modelo 390 es una proyección anual del mismo motor del 303 (suma de Q1..Q4). El resultado neto se llama resultadoAnual (no resultado).
Modelo 347 (operaciones con terceras personas)
curl "https://api.frihet.io/v1/fiscal/modelo/347?year=2026" \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"data": {
"modeloCode": "347",
"model": "347",
"period": "2026",
"year": 2026,
"threshold": 3005.06,
"entries": [
{
"nif": "B12345674",
"nombre": "Cliente Ejemplo SL",
"importeAnual": 7260.0,
"claveOperacion": "B",
"importeQ1": 2420.0,
"importeQ2": 2420.0,
"importeQ3": 2420.0,
"importeQ4": 0
}
],
"summary": {
"totalOperaciones": 7260.0,
"numDeclarados": 1
},
"readonly": true,
"note": "READ-ONLY annual third-party operations recap. Not presented or submitted to AEAT."
},
"meta": {
"requestId": "req_a1b2c3",
"timestamp": "2026-04-12T10:00:00Z"
}
}
Recoge, para el año natural, a cada persona o entidad cuyo importe anual de operaciones supera threshold (3.005,06 €), de mayor a menor importe. Excluye las facturas cancelled y draft. summary.totalOperaciones suma el importeAnual de todas las entradas y summary.numDeclarados las cuenta.
Campos de cada elemento de entries:
| Campo | Tipo | Descripción |
|---|---|---|
nif | string | NIF de la otra parte. Va vacío si solo consta el nombre |
nombre | string | Nombre de la otra parte |
importeAnual | number | Importe del año. En ventas, base imponible más cuota repercutida y recargo de equivalencia; en compras, el importe del gasto |
claveOperacion | string | B para ventas (facturas emitidas) y A para compras (gastos) |
importeQ1, importeQ2, importeQ3, importeQ4 | number | Importe de cada trimestre |
Códigos de respuesta:
| Código | Cuando ocurre |
|---|---|
200 | Resumen calculado correctamente |
400 | Formato de quarter o year inválido. Usa YYYY-Q[1-4] (303/130) o YYYY (390/347) |
404 | Modelo desconocido. Usa 303, 130, 390 o 347 |
Las respuestas de error también 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.
Parámetros de consulta:
| Parámetro | Tipo | Descripción |
|---|---|---|
from | string | Fecha inicio (ISO 8601) |
to | string | Fecha fin (ISO 8601) |
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):
{
"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 }
}
Códigos de error
La API utiliza códigos HTTP estándar. Las respuestas de error incluyen un objeto JSON con los campos error y, opcionalmente, message y details.
| Código | Significado | Descripción |
|---|---|---|
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 método HTTP no esta soportado para este endpoint |
413 | Payload Too Large | El cuerpo de la petición excede 1 MB |
422 | Unprocessable Entity | Datos válidos pero el servidor no puede procesarlos (ej: perfil fiscal no configurado) |
429 | Too Many Requests | Se ha excedido el límite de 100 peticiones por minuto |
500 | Internal Server Error | Error interno del servidor |
Estructura de las respuestas de error
Error de validación (400):
{
"error": "Validation error",
"details": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["clientName"],
"message": "Required"
}
]
}
Los errores de validación usan el formato de Zod. El campo path indica que campo contiene el error y message describe el problema.
Clave invalida o expirada (401):
{
"error": "Invalid or expired API key"
}
Formato de clave invalido (401):
{
"error": "Invalid API key format"
}
Recurso no encontrado (404):
{
"error": "Resource not found"
}
Estado invalido en filtro (400):
{
"error": "Invalid status filter",
"message": "Valid values: draft, sent, paid, overdue, cancelled"
}
Rate limit excedido (429):
{
"error": "Rate limit exceeded",
"message": "Maximum 100 requests per minute",
"retryAfter": 60
}
Error interno (500):
{
"error": "Internal server error"
}
Paginación
Los endpoints de listado devuelven resultados paginados con la siguiente estructura:
{
"data": [],
"total": 142,
"limit": 50,
"offset": 0
}
total: número total de registros que cumplen los filtros aplicadoslimit: número de registros devueltos en esta página (máximo 100)offset: número de registros saltados (máximo 10.000)
Para obtener la siguiente página:
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 (más recientes primero):
- Facturas y presupuestos:
issueDate - Gastos:
date - Clientes y productos:
createdAt
Validación estricta
La API usa validación estricta (Zod strict mode). Las peticiones con campos desconocidos se rechazan con un error 400:
{
"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 orígenes permitidos son:
https://app.frihet.iohttps://frihet.iohttps://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 único de la petición (útil para depuración) |
OAuth para MCP
El endpoint POST /api/oauth/api-key permite provisionar una API key automáticamente a través del flujo OAuth de MCP. El servidor MCP usa este endpoint internamente -- no necesitas llamarlo directamente.
El flujo:
- El usuario se autentica en la app de Frihet
- El cliente MCP envía el token de sesión a
/api/oauth/api-key - El servidor devuelve una nueva clave
fri_xxx...etiquetada como "MCP OAuth" - La clave expira a los 365 días
Límite: 5 claves activas por usuario. Si se supera el límite, el endpoint responde con un 429.
Buenas practicas
- Almacena la API key de forma segura. Nunca la incluyas en código frontend, repositorios públicos o logs.
- Gestiona el rate limiting. Implementa backoff exponencial si recibes un
429. - Usa paginación. No pidas todos los registros de golpe; itera con
limityoffset. - Verifica los códigos de respuesta. No asumas que todas las peticiones serán exitosas.
- Rota las claves periódicamente. Crea una nueva clave, actualiza tu integración y revoca la anterior.
- Usa HTTPS siempre. Todas las peticiones a la API deben ser sobre HTTPS.
- Usa filtros. Los parámetros
status,fromytoreducen la cantidad de datos transferidos. - Aprovecha PATCH. Para actualizaciones parciales, envía solo los campos modificados.
Relacionado: Claves de API · Webhooks · Servidor MCP · SDK y CLI