Saltar al contenido principal

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.

SDK disponible

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:

  1. Ve a Integraciones → API Keys
  2. Pulsa Crear clave
  3. Asigna un nombre descriptivo (por ejemplo, integracion-contabilidad)
  4. Opcionalmente, establece una fecha de expiración en días. Si lo dejas vacío, la clave no caduca
  5. 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:

  1. Crea una nueva clave con el mismo alcance
  2. Actualiza tu integración para usar la nueva clave
  3. Verifica que las peticiones funcionan correctamente
  4. 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:

CabeceraDescripciónEjemplo
X-RateLimit-LimitPeticiones permitidas por minuto100
X-RateLimit-RemainingPeticiones restantes en la ventana actual87
X-RateLimit-ResetTimestamp Unix (segundos) en que la ventana se reinicia1709312400

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 en X-RateLimit-Reset antes de reintentar.
  • Monitoriza X-RateLimit-Remaining para 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.

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

Parámetros de consulta:

ParámetroTipoPor defectoDescripción
limitinteger50Resultados por página (máximo 100)
offsetinteger0Número de resultados a saltar (máximo 10.000)
statusstring--Filtrar por estado: draft, sent, partial, paid, overdue, cancelled
fromstring--Fecha inicio (ISO 8601: YYYY-MM-DD). Filtra por issueDate
tostring--Fecha fin (ISO 8601: YYYY-MM-DD). Filtra por issueDate
clientIdstring--Filtrar por ID de cliente
seriesIdstring--Filtrar por serie de numeración
qstring--Búsqueda de texto (nombre de cliente, número, notas)
cursorstring--Cursor para paginación basada en cursor (ignora offset)
fieldsstring--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:

CampoTipoDescripción
clientNamestringNombre del cliente (max 10.000 caracteres)
itemsarrayLista de líneas de la factura. Cada línea: { description, quantity, unitPrice }

Campos opcionales:

CampoTipoDescripción
statusstringdraft (defecto), sent, partial, paid, overdue, cancelled
issueDatestringFecha de emisión (ISO 8601). Por defecto: hoy
dueDatestringFecha de vencimiento (ISO 8601)
notesstringNotas internas (max 10.000 caracteres)
taxRatenumberPorcentaje de impuesto (0-100). Ej: 21 para IVA 21%

Estructura de cada línea (items[]):

CampoTipoRequeridoDescripción
descriptionstringSiDescripción del concepto (max 10.000 caracteres)
quantitynumberSiCantidad
unitPricenumberSiPrecio 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.

precaución

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:

CampoTipoRequeridoDescripción
recipientEmailstringSiEmail del destinatario (max 255 caracteres)
recipientNamestringNoNombre del destinatario (max 200 caracteres)
customMessagestringNoMensaje personalizado en el cuerpo del email (max 5.000 caracteres)
localestringNoIdioma 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
CampoTipoRequeridoDescripción
paidDatestringNoFecha 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 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):

CampoTipoRequeridoDescripción
reasonstringSiMotivo: refund, discount, error, cancellation, other
reasonDescriptionstringNoDescripción libre del motivo (max. 500 caracteres)
fullCreditbooleanNoSolo true (o se omite). Por defecto: true
issueDatestringNoFecha 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 reasonTipo R asignadoDescripción legal
errorR1Art. 80.1, 80.2 y 80.6 LIVA; art. 15 del Real Decreto 1619/2012
refundR4Resto de causas (devolución, descuento, anulación)
discountR4Resto de causas
cancellationR4Resto de causas
otherR4Resto 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ódigoDescripción
404Factura no encontrada, o su e-factura todavía no se ha generado
Formato del XML

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ódigoDescripción
404Factura no encontrada
422La factura no esta en estado overdue o ya tiene un recargo por demora activo
Configuración de recargos

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ámetroTipoPor defectoDescripción
limitinteger50Resultados por página (máximo 100)
offsetinteger0Número de resultados a saltar
fromstring--Fecha inicio (ISO 8601). Filtra por date
tostring--Fecha fin (ISO 8601). Filtra por date
categorystring--Filtrar por categoría
vendorIdstring--Filtrar por ID de proveedor
qstring--Búsqueda de texto (proveedor, descripción, número)
cursorstring--Cursor para paginación basada en cursor
fieldsstring--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:

CampoTipoDescripción
descriptionstringDescripción del gasto (max 10.000 caracteres)
amountnumberImporte del gasto

Campos opcionales:

CampoTipoDescripción
categorystringCategoría del gasto (max 10.000 caracteres)
datestringFecha del gasto (ISO 8601). Por defecto: hoy
vendorstringProveedor (max 10.000 caracteres)
taxDeductiblebooleanSi 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.

CampoTipoRequeridoDescripción
clientIdstringSiID del cliente al que facturar
markupnumberNoPorcentaje 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ámetroTipoPor defectoDescripción
limitinteger50Resultados por página (máximo 100)
offsetinteger0Número de resultados a saltar
fromstring--Fecha inicio (ISO 8601). Filtra por createdAt
tostring--Fecha fin (ISO 8601). Filtra por createdAt
stagestring--Filtrar por etapa del pipeline: lead, contacted, proposal, active, inactive, lost
qstring--Búsqueda de texto (nombre, email, NIF)
cursorstring--Cursor para paginación basada en cursor
fieldsstring--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:

CampoTipoDescripción
namestringNombre del cliente (max 10.000 caracteres)

Campos opcionales:

CampoTipoDescripción
emailstringEmail de contacto
phonestringTeléfono
taxIdstringNIF/CIF/VAT
addressobjectDirección (ver estructura abajo)

Estructura de address:

CampoTipoDescripción
streetstringCalle y número
citystringCiudad
statestringProvincia o estado
postalCodestringCódigo postal
countrystringPaí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:

CampoTipoDescripción
namestringNombre de la persona de contacto

Campos opcionales:

CampoTipoDescripción
emailstringEmail del contacto
phonestringTeléfono
rolestringCargo o rol (ej. "Director financiero")
isPrimarybooleanSi 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.

Inmutables

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:

CampoTipoDescripción
typestringTipo de actividad: call, email, meeting o task
titlestringTitulo descriptivo de la actividad

Campos opcionales:

CampoTipoDescripción
descriptionstringDescripción detallada
metadataobjectDatos adicionales en formato libre
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 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:

CampoTipoDescripción
contentstringContenido 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:

CampoTipoDescripción
namestringNombre del producto o servicio (max 10.000 caracteres)
unitPricenumberPrecio unitario

Campos opcionales:

CampoTipoDescripción
descriptionstringDescripción (max 10.000 caracteres)
taxRatenumberPorcentaje 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ámetroTipoPor defectoDescripción
limitinteger50Resultados por página (máximo 100)
offsetinteger0Número de resultados a saltar
statusstring--Filtrar por estado: draft, sent, accepted, rejected, expired
fromstring--Fecha inicio (ISO 8601). Filtra por issueDate
tostring--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:

CampoTipoDescripción
clientNamestringNombre del cliente (max 10.000 caracteres)
itemsarrayLíneas del presupuesto. Cada línea: { description, quantity, unitPrice }

Campos opcionales:

CampoTipoDescripción
validUntilstringFecha de validez (ISO 8601)
notesstringNotas o condiciones (max 10.000 caracteres)
statusstringdraft (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:

CampoTipoDescripción
namestringNombre del proveedor

Campos opcionales:

CampoTipoDescripción
emailstringEmail de contacto
phonestringTeléfono
taxIdstringNIF/CIF/VAT
addressobjectDirecció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ámetroTipoPor defectoDescripción
limitinteger50Resultados por página (máximo 100)
offsetinteger0Número de resultados a saltar
clientIdstring--Filtrar por ID de cliente
statusstring--Filtrar por estado: pending, partial, applied, refunded
fromstring--Fecha inicio (ISO 8601: YYYY-MM-DD). Filtra por date
tostring--Fecha fin (ISO 8601: YYYY-MM-DD). Filtra por date
cursorstring--Cursor para paginación basada en cursor (ignora offset)
fieldsstring--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:

CampoTipoDescripción
clientIdstringID del cliente
amountnumberImporte del depósito (debe ser positivo)

Campos opcionales:

CampoTipoDescripción
currencystringCódigo ISO 4217 (3 caracteres). Por defecto: EUR
datestringFecha del depósito (ISO 8601: YYYY-MM-DD). Por defecto: hoy
descriptionstringDescripción o concepto del depósito (max 10.000 caracteres)
paymentMethodstringMétodo de pago: bank_transfer, card, cash, other
referencestringReferencia del pago (max 500 caracteres)
notesstringNotas 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

precaución

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.

CampoTipoRequeridoDescripción
invoiceIdstringSiID de la factura a la que aplicar el depósito
amountnumberSiImporte 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ódigoDescripción
404Depósito o factura no encontrados
422El 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.

CampoTipoRequeridoDescripción
amountnumberNoImporte a reembolsar. Si se omite, se reembolsa el remainingAmount completo
reasonstringNoMotivo 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ódigoDescripción
404Depósito no encontrado
422El 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:

ConceptoLímite
Elementos por lote50 máximo
Tamaño de petición1 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:

EscenarioResultado
Primera petición con la claveSe 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 registrar409 IDEMPOTENCY_REQUEST_IN_PROGRESS
La misma clave en una operación distinta409 IDEMPOTENCY_KEY_REUSED — la segunda operación no se ejecuta
Misma clave después de 24hSe 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.

Diferencias con /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}.

consejo

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.

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 propósito. Cada respuesta incluye readonly: true y una note que lo recuerda.

Parámetros de ruta:

ParámetroValoresDescripción
model303, 130, 390, 347Modelo fiscal a calcular

Parámetros de consulta:

ParámetroAplica aFormatoDescripción
quarter303, 130YYYY-Q[1-4]Trimestre. Por defecto, el trimestre actual
year390, 347YYYYAño natural. Por defecto, el año actual (en el 390, suma de Q1 a Q4)
modeloCode y model

La 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:

CampoTipoDescripción
baseImponiblenumberBase de operaciones interiores sujetas a IVA
cuotaRepercutidanumberIVA devengado (repercutido a clientes)
baseDeduciblenumberBase de gastos con IVA deducible
cuotaDeduciblenumberIVA soportado deducible
resultadonumbercuotaRepercutida − cuotaDeducible. Positivo = a pagar; negativo = a devolver/compensar
baseExentanumberBase de operaciones exentas (se declara, cuota cero)
baseNoSujetaORCnumberBase de operaciones intracomunitarias / exportación / inversión del sujeto pasivo (ISP)
outOfScopeobjectOperaciones 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:

CampoTipoDescripción
ingresosnumberIngresos acumulados desde el 1 de enero (casilla 01)
gastosnumberGastos deducibles acumulados desde el 1 de enero (casilla 02)
rendimientoNetonumberingresos − gastos (casilla 03)
pagoFraccionadonumberCasilla 12: c04 − c05 − c06 (casilla 07), o 0 si sale negativa. Para estimación directa, normal o simplificada
retencionesSoportadasnumberIRPF retenido por los clientes en las facturas emitidas, acumulado desde el 1 de enero (casilla 06)
basisstringSiempre "ytd": casillas acumuladas desde el 1 de enero
yearnumberEjercicio
quarternumberTrimestre (1 a 4)
porcentajenumberPorcentaje que la casilla 04 aplica al rendimiento neto positivo (20)
casillasobjectCasillas c01 a c07 y c12. De c13 a c19 van a null
pagosAnterioresPorTrimestrearrayCasilla 07 de cada trimestre anterior del año: quarter y c07
excludedobjectNúmero de facturas y de gastos sin fecha válida, que no entran en el cálculo
notModelledarrayLo que el cálculo no incluye
sourceUrlstringInstrucciones 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 resultado

El 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:

CampoTipoDescripción
nifstringNIF de la otra parte. Va vacío si solo consta el nombre
nombrestringNombre de la otra parte
importeAnualnumberImporte del año. En ventas, base imponible más cuota repercutida y recargo de equivalencia; en compras, el importe del gasto
claveOperacionstringB para ventas (facturas emitidas) y A para compras (gastos)
importeQ1, importeQ2, importeQ3, importeQ4numberImporte de cada trimestre

Códigos de respuesta:

CódigoCuando ocurre
200Resumen calculado correctamente
400Formato de quarter o year inválido. Usa YYYY-Q[1-4] (303/130) o YYYY (390/347)
404Modelo 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ámetroTipoDescripción
fromstringFecha inicio (ISO 8601)
tostringFecha 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ódigoSignificadoDescripción
400Bad RequestFalta un campo requerido, formato incorrecto o campo no permitido
401UnauthorizedLa API key no se ha proporcionado, es invalida, tiene formato incorrecto o ha expirado
403ForbiddenLa API key no tiene permisos para acceder a este recurso
404Not FoundEl recurso solicitado no existe
405Method Not AllowedEl método HTTP no esta soportado para este endpoint
413Payload Too LargeEl cuerpo de la petición excede 1 MB
422Unprocessable EntityDatos válidos pero el servidor no puede procesarlos (ej: perfil fiscal no configurado)
429Too Many RequestsSe ha excedido el límite de 100 peticiones por minuto
500Internal Server ErrorError 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 aplicados
  • limit: 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.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:

CabeceraValor
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection1; mode=block
X-API-VersionVersion de la API (2026-03-18)
X-Request-IdID ú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:

  1. El usuario se autentica en la app de Frihet
  2. El cliente MCP envía el token de sesión 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 días

Límite: 5 claves activas por usuario. Si se supera el límite, el endpoint responde con un 429.


Buenas practicas​

  1. Almacena la API key de forma segura. Nunca la incluyas en código frontend, repositorios públicos o logs.
  2. Gestiona el rate limiting. Implementa backoff exponencial si recibes un 429.
  3. Usa paginación. No pidas todos los registros de golpe; itera con limit y offset.
  4. Verifica los códigos de respuesta. No asumas que todas las peticiones serán exitosas.
  5. Rota las claves periódicamente. Crea una nueva clave, actualiza tu integración y revoca la anterior.
  6. Usa HTTPS siempre. Todas las peticiones a la API deben ser sobre HTTPS.
  7. Usa filtros. Los parámetros status, from y to reducen la cantidad de datos transferidos.
  8. Aprovecha PATCH. Para actualizaciones parciales, envía solo los campos modificados.

Relacionado: Claves de API · Webhooks · Servidor MCP · SDK y CLI