REST API
The Frihet REST API lets you access and manipulate the resources in your account programmatically. All communication is over HTTPS and responses are JSON.
Base URL
https://api.frihet.io/v1
All endpoints in this reference are relative to this base URL.
Discovery: A GET request to the root (https://api.frihet.io/) returns the main links without authentication:
{
"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"
}
The OpenAPI 3.1 specification is available at https://api.frihet.io/openapi.yaml.
For TypeScript/JavaScript, the official SDK simplifies integration:
npm install @frihet/sdk
import Frihet from '@frihet/sdk';
const frihet = new Frihet({ apiKey: 'fri_...' });
const invoices = await frihet.invoices.list({ status: 'overdue' });
Repository: github.com/Frihet-io/frihet-sdk
Authentication
Every request must include an API key in the X-API-Key header. Keys are created from Settings > Developers > API Keys in the Frihet dashboard.
Keys use the prefix fri_ followed by 32 random bytes encoded in base64url. Example: fri_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345678.
curl https://api.frihet.io/v1/clients \
-H "X-API-Key: fri_your-key-here"
Alternatively, you can send the key as a Bearer token in the Authorization header:
curl https://api.frihet.io/v1/clients \
-H "Authorization: Bearer fri_your-key-here"
Key security
- The plaintext key is shown only once, at creation time. It cannot be recovered afterward.
- The server stores a SHA-256 hash of the key. Even in the event of a data breach, the original key is not recoverable.
- You can create keys with a configurable expiration date.
- If you suspect a key has been compromised, revoke it immediately from the dashboard.
Key lifecycle
Creating a key:
- Go to Settings > Developers > API Keys
- Click Create key
- Assign a descriptive name (e.g.,
accounting-integration) - Optionally, set an expiration date in days. If left empty, the key does not expire
- Copy the key immediately -- you will not be able to see it again
Expiration:
Keys with an expiration date stop working automatically when the date is reached. Requests with an expired key receive a 401 Unauthorized.
Revocation:
You can revoke a key at any time from Settings > Developers > API Keys. Revocation is immediate and irreversible: in-flight requests with that key will fail from that moment on.
Key rotation:
To rotate a key without service interruption:
- Create a new key with the same scope
- Update your integration to use the new key
- Verify that requests work correctly
- Revoke the old key
Monitoring:
Each key shows the last used date in the Settings panel. Periodically review inactive keys and revoke those no longer in use.
Rate limiting
Each API key has a limit of 100 requests per minute. If exceeded, the API responds with a 429:
{
"error": "Rate limit exceeded",
"message": "Maximum 100 requests per minute",
"retryAfter": 60
}
Rate limiting headers
All API responses include headers so you can manage the limit proactively:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Allowed requests per minute | 100 |
X-RateLimit-Remaining | Remaining requests in the current window | 87 |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets | 1709312400 |
Example response with rate limiting headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1709312400
Content-Type: application/json
Handling 429 responses:
When you receive a 429, use the headers to calculate how long to wait before retrying:
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;
}
Recommendations:
- If you receive a
429, wait until the timestamp indicated inX-RateLimit-Resetbefore retrying. - Monitor
X-RateLimit-Remainingto throttle requests before hitting the limit. - Spread requests over time rather than sending bursts.
Request size
The body of POST, PUT, and PATCH requests cannot exceed 1 MB. Larger requests receive a 413.
Resources
The API exposes 7 main resources: invoices, expenses, clients, products, quotes, vendors, and webhooks. All support full CRUD operations (GET, POST, PUT/PATCH, DELETE). Additionally, clients have CRM subcollections: contacts, activities, and notes.
Both PUT and PATCH accept partial updates. You do not need to send the full resource -- only the fields you want to modify.
Invoices (/invoices)
List invoices
GET /v1/invoices
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Results per page (maximum 100) |
offset | integer | 0 | Number of results to skip (maximum 10,000) |
status | string | -- | Filter by status: draft, sent, paid, overdue, cancelled |
from | string | -- | Start date (ISO 8601: YYYY-MM-DD). Filters by issueDate |
to | string | -- | End date (ISO 8601: YYYY-MM-DD). Filters by issueDate |
Example:
curl "https://api.frihet.io/v1/invoices?limit=10&status=paid&from=2026-01-01&to=2026-03-31" \
-H "X-API-Key: fri_your-key-here"
Response (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
}
Get invoice
GET /v1/invoices/:id
curl https://api.frihet.io/v1/invoices/abc123 \
-H "X-API-Key: fri_your-key-here"
Response (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"
}
Create invoice
POST /v1/invoices
Required fields:
| Field | Type | Description |
|---|---|---|
clientName | string | Client name (max 10,000 characters) |
items | array | Invoice line items. Each item: { description, quantity, unitPrice } |
Optional fields:
| Field | Type | Description |
|---|---|---|
status | string | draft (default), sent, paid, overdue, cancelled |
issueDate | string | Issue date (ISO 8601). Default: today |
dueDate | string | Due date (ISO 8601) |
notes | string | Internal notes (max 10,000 characters) |
taxRate | number | Tax percentage (0-100). E.g., 21 for 21% VAT |
Structure of each line item (items[]):
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | Item description (max 10,000 characters) |
quantity | number | Yes | Quantity |
unitPrice | number | Yes | Unit price |
curl -X POST https://api.frihet.io/v1/invoices \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"clientName": "Acme S.L.",
"items": [
{ "description": "Desarrollo web", "quantity": 40, "unitPrice": 60 }
],
"dueDate": "2026-03-01",
"taxRate": 21,
"notes": "Proyecto Q1 2026"
}'
Response (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 Q1 2026",
"createdAt": "2026-02-12T09:00:00.000Z",
"updatedAt": "2026-02-12T09:00:00.000Z"
}
Update invoice (PUT or PATCH)
PUT /v1/invoices/:id
PATCH /v1/invoices/:id
You only need to send the fields you want to modify. Fields not included remain unchanged.
curl -X PATCH https://api.frihet.io/v1/invoices/def456 \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"status": "sent",
"notes": "Enviada al cliente"
}'
Response (200): Updated invoice object.
If you send items, you must send the complete array -- partial updates of individual line items are not supported.
Delete invoice
DELETE /v1/invoices/:id
curl -X DELETE https://api.frihet.io/v1/invoices/def456 \
-H "X-API-Key: fri_your-key-here"
Response: 204 No Content
Download invoice as PDF
GET /v1/invoices/:id/pdf
Returns the invoice PDF as application/pdf with a Content-Disposition: attachment header.
curl -o invoice.pdf https://api.frihet.io/v1/invoices/abc123/pdf \
-H "X-API-Key: fri_your-key-here"
Send invoice by email
POST /v1/invoices/:id/send
Sends the invoice to the specified recipient via Resend. If the invoice is in draft status, it is automatically updated to sent.
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
recipientEmail | string | Yes | Recipient email address (max 255 characters) |
recipientName | string | No | Recipient name (max 200 characters) |
customMessage | string | No | Custom message in the email body (max 5,000 characters) |
locale | string | No | Email language: es (default) or en |
curl -X POST https://api.frihet.io/v1/invoices/abc123/send \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"recipientEmail": "admin@acme.es",
"recipientName": "Departamento de Contabilidad",
"locale": "es"
}'
Response (200):
{ "success": true, "messageId": "re_abc123..." }
Mark invoice as paid
POST /v1/invoices/:id/paid
| Field | Type | Required | Description |
|---|---|---|---|
paidDate | string | No | Payment date (ISO 8601). Default: today |
curl -X POST https://api.frihet.io/v1/invoices/abc123/paid \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{ "paidDate": "2026-03-15" }'
Response (200):
{ "success": true, "status": "paid", "paidAt": "2026-03-15" }
Expenses (/expenses)
List expenses
GET /v1/expenses
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Results per page (maximum 100) |
offset | integer | 0 | Number of results to skip |
from | string | -- | Start date (ISO 8601). Filters by date |
to | string | -- | End date (ISO 8601). Filters by date |
curl "https://api.frihet.io/v1/expenses?limit=20&from=2026-01-01" \
-H "X-API-Key: fri_your-key-here"
Response (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
}
Get expense
GET /v1/expenses/:id
Create expense
POST /v1/expenses
Required fields:
| Field | Type | Description |
|---|---|---|
description | string | Expense description (max 10,000 characters) |
amount | number | Expense amount |
Optional fields:
| Field | Type | Description |
|---|---|---|
category | string | Expense category (max 10,000 characters) |
date | string | Expense date (ISO 8601). Default: today |
vendor | string | Vendor name (max 10,000 characters) |
taxDeductible | boolean | Whether the expense is tax-deductible |
curl -X POST https://api.frihet.io/v1/expenses \
-H "X-API-Key: fri_your-key-here" \
-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
}'
Response (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"
}
Update expense (PUT or PATCH)
PUT /v1/expenses/:id
PATCH /v1/expenses/:id
curl -X PATCH https://api.frihet.io/v1/expenses/exp789 \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{ "amount": 65.99, "taxDeductible": false }'
Response (200): Updated expense object.
Delete expense
DELETE /v1/expenses/:id
Response: 204 No Content
Clients (/clients)
List clients
GET /v1/clients
Accepts limit, offset, from, and to (filters by createdAt).
curl "https://api.frihet.io/v1/clients?limit=50" \
-H "X-API-Key: fri_your-key-here"
Get client
GET /v1/clients/:id
Create client
POST /v1/clients
Required fields:
| Field | Type | Description |
|---|---|---|
name | string | Client name (max 10,000 characters) |
Optional fields:
| Field | Type | Description |
|---|---|---|
email | string | Contact email |
phone | string | Phone number |
taxId | string | Tax ID (NIF/CIF/VAT) |
address | object | Address (see structure below) |
address structure:
| Field | Type | Description |
|---|---|---|
street | string | Street and number |
city | string | City |
state | string | State or province |
postalCode | string | Postal code |
country | string | Country |
All address fields are optional.
curl -X POST https://api.frihet.io/v1/clients \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme S.L.",
"email": "admin@acme.es",
"taxId": "B12345678",
"address": {
"street": "Calle Gran Via 42",
"city": "Madrid",
"postalCode": "28013",
"country": "ES"
}
}'
Response (201):
{
"id": "cli001",
"name": "Acme S.L.",
"email": "admin@acme.es",
"taxId": "B12345678",
"address": {
"street": "Calle Gran Via 42",
"city": "Madrid",
"postalCode": "28013",
"country": "ES"
},
"createdAt": "2026-02-12T09:30:00.000Z",
"updatedAt": "2026-02-12T09:30:00.000Z"
}
Update client (PUT or PATCH)
PUT /v1/clients/:id
PATCH /v1/clients/:id
curl -X PATCH https://api.frihet.io/v1/clients/cli001 \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{ "phone": "+34 912 345 678" }'
Response (200): Updated client object.
Delete client
DELETE /v1/clients/:id
Response: 204 No Content
CRM: Contacts, Activities, and Notes
Clients have three subcollections for CRM relationship management: contacts, activities, and notes. All endpoints require a valid clientId in the URL.
Contacts (/v1/clients/:id/contacts)
List contacts
GET /v1/clients/:id/contacts
curl "https://api.frihet.io/v1/clients/cli001/contacts" \
-H "X-API-Key: fri_your-key-here"
Get contact
GET /v1/clients/:id/contacts/:contactId
curl "https://api.frihet.io/v1/clients/cli001/contacts/con001" \
-H "X-API-Key: fri_your-key-here"
Create contact
POST /v1/clients/:id/contacts
Required fields:
| Field | Type | Description |
|---|---|---|
name | string | Contact person name |
Optional fields:
| Field | Type | Description |
|---|---|---|
email | string | Contact email |
phone | string | Phone number |
role | string | Job title or role (e.g., "CFO") |
isPrimary | boolean | Whether this is the primary contact for the client |
curl -X POST https://api.frihet.io/v1/clients/cli001/contacts \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"name": "Maria Garcia",
"email": "maria@acme.es",
"phone": "+34 612 345 678",
"role": "CFO",
"isPrimary": true
}'
Response (201):
{
"id": "con001",
"name": "Maria Garcia",
"email": "maria@acme.es",
"phone": "+34 612 345 678",
"role": "CFO",
"isPrimary": true,
"createdAt": "2026-03-15T10:00:00.000Z",
"updatedAt": "2026-03-15T10:00:00.000Z"
}
Update contact
PATCH /v1/clients/:id/contacts/:contactId
curl -X PATCH https://api.frihet.io/v1/clients/cli001/contacts/con001 \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{ "role": "CEO" }'
Response (200): Updated contact object.
Delete contact
DELETE /v1/clients/:id/contacts/:contactId
curl -X DELETE https://api.frihet.io/v1/clients/cli001/contacts/con001 \
-H "X-API-Key: fri_your-key-here"
Response: 204 No Content
Activities (/v1/clients/:id/activities)
The activity timeline tracks interactions with a client. System activities (like invoice_created, quote_sent, etc.) are generated automatically. You can also create manual activities.
Activities are immutable. They cannot be updated or deleted once created.
List activities
GET /v1/clients/:id/activities
curl "https://api.frihet.io/v1/clients/cli001/activities" \
-H "X-API-Key: fri_your-key-here"
Get activity
GET /v1/clients/:id/activities/:activityId
curl "https://api.frihet.io/v1/clients/cli001/activities/act001" \
-H "X-API-Key: fri_your-key-here"
Create activity
POST /v1/clients/:id/activities
Required fields:
| Field | Type | Description |
|---|---|---|
type | string | Activity type: call, email, meeting, or task |
title | string | Descriptive title for the activity |
Optional fields:
| Field | Type | Description |
|---|---|---|
description | string | Detailed description |
metadata | object | Free-form additional data |
The types call, email, meeting, and task are for manually created activities. System types like invoice_created, quote_sent, or expense_linked are generated automatically and cannot be created via API.
curl -X POST https://api.frihet.io/v1/clients/cli001/activities \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"type": "call",
"title": "Follow-up call on Q2 budget",
"description": "Discussed budget terms. Awaiting confirmation.",
"metadata": {
"duration": "15min",
"outcome": "pending"
}
}'
Response (201):
{
"id": "act001",
"type": "call",
"title": "Follow-up call on Q2 budget",
"description": "Discussed budget terms. Awaiting confirmation.",
"metadata": {
"duration": "15min",
"outcome": "pending"
},
"createdAt": "2026-03-15T14:30:00.000Z"
}
Notes (/v1/clients/:id/notes)
List notes
GET /v1/clients/:id/notes
curl "https://api.frihet.io/v1/clients/cli001/notes" \
-H "X-API-Key: fri_your-key-here"
Get note
GET /v1/clients/:id/notes/:noteId
curl "https://api.frihet.io/v1/clients/cli001/notes/note001" \
-H "X-API-Key: fri_your-key-here"
Create note
POST /v1/clients/:id/notes
Required fields:
| Field | Type | Description |
|---|---|---|
content | string | Note content |
curl -X POST https://api.frihet.io/v1/clients/cli001/notes \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"content": "Client interested in Business plan. Follow up in April for renewal."
}'
Response (201):
{
"id": "note001",
"content": "Client interested in Business plan. Follow up in April for renewal.",
"createdAt": "2026-03-15T16:00:00.000Z",
"updatedAt": "2026-03-15T16:00:00.000Z"
}
Update note
PATCH /v1/clients/:id/notes/:noteId
curl -X PATCH https://api.frihet.io/v1/clients/cli001/notes/note001 \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"content": "Client interested in Business plan. Meeting confirmed April 5th."
}'
Response (200): Updated note object.
Delete note
DELETE /v1/clients/:id/notes/:noteId
curl -X DELETE https://api.frihet.io/v1/clients/cli001/notes/note001 \
-H "X-API-Key: fri_your-key-here"
Response: 204 No Content
Products (/products)
List products
GET /v1/products
Accepts limit, offset, from, and to (filters by createdAt).
Get product
GET /v1/products/:id
Create product
POST /v1/products
Required fields:
| Field | Type | Description |
|---|---|---|
name | string | Product or service name (max 10,000 characters) |
unitPrice | number | Unit price |
Optional fields:
| Field | Type | Description |
|---|---|---|
description | string | Description (max 10,000 characters) |
taxRate | number | Tax percentage (0-100) |
curl -X POST https://api.frihet.io/v1/products \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{
"name": "Hora de consultoria",
"unitPrice": 75,
"description": "Consultoria estrategica",
"taxRate": 21
}'
Response (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"
}
Update product (PUT or PATCH)
PUT /v1/products/:id
PATCH /v1/products/:id
curl -X PATCH https://api.frihet.io/v1/products/prod001 \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-d '{ "unitPrice": 85 }'
Response (200): Updated product object.
Delete product
DELETE /v1/products/:id
Response: 204 No Content
Quotes (/quotes)
List quotes
GET /v1/quotes
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Results per page (maximum 100) |
offset | integer | 0 | Number of results to skip |
status | string | -- | Filter by status: draft, sent, accepted, rejected, expired |
from | string | -- | Start date (ISO 8601). Filters by issueDate |
to | string | -- | End date (ISO 8601). Filters by issueDate |
curl "https://api.frihet.io/v1/quotes?status=sent" \
-H "X-API-Key: fri_your-key-here"
Get quote
GET /v1/quotes/:id
Create quote
POST /v1/quotes
Required fields:
| Field | Type | Description |
|---|---|---|
clientName | string | Client name (max 10,000 characters) |
items | array | Quote line items. Each item: { description, quantity, unitPrice } |
Optional fields:
| Field | Type | Description |
|---|---|---|
validUntil | string | Expiration date (ISO 8601) |
notes | string | Notes or terms (max 10,000 characters) |
status | string | draft (default), sent, accepted, rejected, expired |
curl -X POST https://api.frihet.io/v1/quotes \
-H "X-API-Key: fri_your-key-here" \
-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"
}'
Response (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"
}
Update quote (PUT or PATCH)
PUT /v1/quotes/:id
PATCH /v1/quotes/:id
Delete quote
DELETE /v1/quotes/:id
Response: 204 No Content
Download quote as PDF
GET /v1/quotes/:id/pdf
Works the same as /invoices/:id/pdf. Returns application/pdf.
curl -o quote.pdf https://api.frihet.io/v1/quotes/quo001/pdf \
-H "X-API-Key: fri_your-key-here"
Send quote by email
POST /v1/quotes/:id/send
Same fields as /invoices/:id/send (recipientEmail, recipientName, customMessage, locale). If the quote is in draft status, it is automatically updated to sent.
Batch operations (/batch)
All main resources support batch creation. Send an array of up to 50 items in a single request.
POST /v1/{resource}/batch
Supported resources: invoices, expenses, clients, products, quotes
Request body:
{
"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_your-key-here" \
-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 }] }
]
}'
Response (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 }
}
If any item fails validation, the rest are still created. Individual errors are returned in the results array:
{
"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 }
}
Limits:
| Concept | Limit |
|---|---|
| Items per batch | 50 maximum |
| Request size | 1 MB maximum |
Idempotency
POST requests accept an Idempotency-Key header to prevent duplicate resource creation during network retries. If the same key is sent within the next 24 hours, the API returns the original response without executing the operation again.
curl -X POST https://api.frihet.io/v1/invoices \
-H "X-API-Key: fri_your-key-here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"clientName": "Acme SL",
"items": [{ "description": "Consultoria", "quantity": 10, "unitPrice": 95 }]
}'
Behavior:
| Scenario | Result |
|---|---|
| First request with the key | Resource is created normally |
| Repeated request with the same key (within 24h) | Original response is returned, no duplicate created |
| Same key after 24h | Treated as a new request |
Response header:
When the API detects a repeated key, it includes the X-Idempotent-Replayed: true header so the consumer knows the response is a replay.
HTTP/1.1 201 Created
X-Idempotent-Replayed: true
Content-Type: application/json
Requirements:
- The key must be at most 64 characters
- We recommend using UUID v4
- Only applies to
POSTrequests (resource creation)
Search
List endpoints support full-text search via the q parameter:
curl "https://api.frihet.io/v1/invoices?q=acme" \
-H "X-API-Key: fri_your-key-here"
The search applies to the main text fields of the resource (client name, description, notes, etc.). It can be combined with existing filters (status, from, to).
Intelligence endpoints
These endpoints provide aggregated data and business context. They are especially useful for AI agents and external dashboards.
Business context (/context)
GET /v1/context
Returns a comprehensive business summary, designed to feed AI agents with the context needed to make informed decisions. Includes financial summary, recent activity, alerts, and tax configuration.
curl https://api.frihet.io/v1/context \
-H "X-API-Key: fri_your-key-here"
Response (200):
{
"business": {
"name": "BRTHLS Studio",
"taxId": "12345678A",
"fiscalZone": "canarias",
"currency": "EUR"
},
"summary": {
"revenue": { "invoiced": 15000, "paid": 12000, "pending": 2000, "overdue": 1000 },
"expenses": { "total": 4500 },
"profit": 7500,
"counts": { "invoices": 25, "clients": 12, "products": 5 }
},
"recentActivity": [
{ "type": "invoice.created", "id": "inv_001", "description": "Factura para Acme SL", "timestamp": "2026-03-18T10:00:00Z" },
{ "type": "expense.created", "id": "exp_042", "description": "Adobe Creative Cloud", "timestamp": "2026-03-17T14:30:00Z" }
],
"alerts": [
{ "type": "overdue", "count": 2, "amount": 1000 },
{ "type": "tax_deadline", "model": "303", "dueDate": "2026-04-20" }
]
}
Monthly P&L (/monthly)
GET /v1/monthly?month=YYYY-MM
Returns the profit & loss statement for a specific month: invoiced revenue, expenses by category, net profit, and comparison with the previous month.
curl "https://api.frihet.io/v1/monthly?month=2026-02" \
-H "X-API-Key: fri_your-key-here"
Response (200):
{
"month": "2026-02",
"revenue": {
"invoiced": 8500.00,
"collected": 6200.00,
"outstanding": 2300.00
},
"expenses": {
"total": 3100.00,
"byCategory": {
"software": 450.00,
"marketing": 800.00,
"office": 350.00,
"professional_services": 1500.00
}
},
"profit": 5400.00,
"comparison": {
"revenueChange": 12.5,
"expenseChange": -5.2,
"profitChange": 22.1
}
}
Quarterly tax figures (/quarterly)
GET /v1/quarterly?quarter=YYYY-Q1
Returns the tax figures for a quarter, prepared for filing Modelo 303 (VAT) and Modelo 130 (income tax). Includes taxable bases, accrued tax, deductible tax, and the settlement result.
curl "https://api.frihet.io/v1/quarterly?quarter=2026-Q1" \
-H "X-API-Key: fri_your-key-here"
Response (200):
{
"quarter": "2026-Q1",
"period": { "from": "2026-01-01", "to": "2026-03-31" },
"modelo303": {
"baseImponible21": 12000.00,
"cuotaDevengada21": 2520.00,
"baseImponible10": 0,
"cuotaDevengada10": 0,
"baseImponible4": 0,
"cuotaDevengada4": 0,
"totalDevengado": 2520.00,
"ivaDeducible": 980.00,
"resultado": 1540.00
},
"modelo130": {
"ingresos": 12000.00,
"gastos": 4500.00,
"rendimientoNeto": 7500.00,
"porcentaje": 20,
"cuota": 1500.00,
"retenciones": 0,
"pagosAnteriores": 0,
"resultado": 1500.00
}
}
The /context, /monthly, and /quarterly endpoints are designed to be the ideal entry point for AI agents. They provide the necessary information in a single call, without needing to query multiple individual endpoints.
Financial dashboard (/summary)
GET /v1/summary
Returns a financial summary of the business: revenue, expenses, profit, and counters.
Query parameters:
| Parameter | Type | Description |
|---|---|---|
from | string | Start date (ISO 8601) |
to | string | End date (ISO 8601) |
curl "https://api.frihet.io/v1/summary?from=2026-01-01&to=2026-03-31" \
-H "X-API-Key: fri_your-key-here"
Response (200):
{
"period": { "from": "2026-01-01", "to": "2026-03-31" },
"revenue": {
"invoiced": 15000.00,
"paid": 12000.00,
"pending": 2000.00,
"overdue": 1000.00
},
"expenses": { "total": 4500.00 },
"profit": 7500.00,
"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.00 }
}
Error codes
The API uses standard HTTP status codes. Error responses include a JSON object with error and, optionally, message and details fields.
| Code | Meaning | Description |
|---|---|---|
400 | Bad Request | A required field is missing, format is incorrect, or a field is not allowed |
401 | Unauthorized | API key not provided, invalid, incorrectly formatted, or expired |
403 | Forbidden | API key does not have permission to access this resource |
404 | Not Found | The requested resource does not exist |
405 | Method Not Allowed | HTTP method is not supported for this endpoint |
413 | Payload Too Large | Request body exceeds 1 MB |
422 | Unprocessable Entity | Data is valid but the server cannot process it (e.g., tax profile not configured) |
429 | Too Many Requests | Rate limit of 100 requests per minute exceeded |
500 | Internal Server Error | Server-side error |
Error response structure
Validation error (400):
{
"error": "Validation error",
"details": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["clientName"],
"message": "Required"
}
]
}
Validation errors use the Zod format. The path field indicates which field contains the error and message describes the problem.
Invalid or expired key (401):
{
"error": "Invalid or expired API key"
}
Invalid key format (401):
{
"error": "Invalid API key format"
}
Resource not found (404):
{
"error": "Resource not found"
}
Invalid status filter (400):
{
"error": "Invalid status filter",
"message": "Valid values: draft, sent, paid, overdue, cancelled"
}
Rate limit exceeded (429):
{
"error": "Rate limit exceeded",
"message": "Maximum 100 requests per minute",
"retryAfter": 60
}
Internal error (500):
{
"error": "Internal server error"
}
Pagination
List endpoints return paginated results with the following structure:
{
"data": [],
"total": 142,
"limit": 50,
"offset": 0
}
total: total number of records matching the applied filterslimit: number of records returned in this page (maximum 100)offset: number of records skipped (maximum 10,000)
To get the next page:
curl "https://api.frihet.io/v1/invoices?limit=50&offset=50" \
-H "X-API-Key: fri_your-key-here"
Results are ordered by the resource's natural date in descending order (most recent first):
- Invoices and quotes:
issueDate - Expenses:
date - Clients and products:
createdAt
Strict validation
The API uses strict validation (Zod strict mode). Requests with unknown fields are rejected with a 400 error:
{
"error": "Validation error",
"details": [
{
"code": "unrecognized_keys",
"keys": ["campoInventado"],
"path": [],
"message": "Unrecognized key(s) in object: 'campoInventado'"
}
]
}
This prevents silent errors from typos in field names.
CORS
The API supports CORS for browser requests. Allowed origins are:
https://app.frihet.iohttps://frihet.iohttps://www.frihet.io
For server-to-server integrations, CORS is not relevant. If you need to access the API from a different domain in the browser, use a proxy in your backend.
Security headers
All responses include security headers:
| Header | Value |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
X-XSS-Protection | 1; mode=block |
X-Request-Id | Unique request ID (useful for debugging) |
OAuth for MCP
The POST /api/oauth/api-key endpoint allows provisioning an API key automatically through the MCP OAuth flow. The MCP server uses this endpoint internally -- you do not need to call it directly.
The flow:
- The user authenticates via Firebase Auth
- The MCP client sends the Firebase token to
/api/oauth/api-key - The server returns a new
fri_xxx...key labeled as "MCP OAuth" - The key expires after 365 days
Limit: 5 active keys per user. If the limit is exceeded, the endpoint responds with a 429.
Best practices
- Store your API key securely. Never include it in frontend code, public repositories, or logs.
- Handle rate limiting. Implement exponential backoff when you receive a
429. - Use pagination. Don't request all records at once; iterate with
limitandoffset. - Check response codes. Don't assume every request will succeed.
- Rotate keys periodically. Create a new key, update your integration, then revoke the old one.
- Always use HTTPS. All API requests must go over HTTPS.
- Use filters. The
status,from, andtoparameters reduce the amount of data transferred. - Use PATCH. For partial updates, send only the modified fields.