Skip to main content

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.

SDK available

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:

  1. Go to Settings > Developers > API Keys
  2. Click Create key
  3. Assign a descriptive name (e.g., accounting-integration)
  4. Optionally, set an expiration date in days. If left empty, the key does not expire
  5. 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:

  1. Create a new key with the same scope
  2. Update your integration to use the new key
  3. Verify that requests work correctly
  4. 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:

HeaderDescriptionExample
X-RateLimit-LimitAllowed requests per minute100
X-RateLimit-RemainingRemaining requests in the current window87
X-RateLimit-ResetUnix timestamp (seconds) when the window resets1709312400

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 in X-RateLimit-Reset before retrying.
  • Monitor X-RateLimit-Remaining to 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.

PATCH vs PUT

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:

ParameterTypeDefaultDescription
limitinteger50Results per page (maximum 100)
offsetinteger0Number of results to skip (maximum 10,000)
statusstring--Filter by status: draft, sent, paid, overdue, cancelled
fromstring--Start date (ISO 8601: YYYY-MM-DD). Filters by issueDate
tostring--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:

FieldTypeDescription
clientNamestringClient name (max 10,000 characters)
itemsarrayInvoice line items. Each item: { description, quantity, unitPrice }

Optional fields:

FieldTypeDescription
statusstringdraft (default), sent, paid, overdue, cancelled
issueDatestringIssue date (ISO 8601). Default: today
dueDatestringDue date (ISO 8601)
notesstringInternal notes (max 10,000 characters)
taxRatenumberTax percentage (0-100). E.g., 21 for 21% VAT

Structure of each line item (items[]):

FieldTypeRequiredDescription
descriptionstringYesItem description (max 10,000 characters)
quantitynumberYesQuantity
unitPricenumberYesUnit 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.

caution

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:

FieldTypeRequiredDescription
recipientEmailstringYesRecipient email address (max 255 characters)
recipientNamestringNoRecipient name (max 200 characters)
customMessagestringNoCustom message in the email body (max 5,000 characters)
localestringNoEmail 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
FieldTypeRequiredDescription
paidDatestringNoPayment 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:

ParameterTypeDefaultDescription
limitinteger50Results per page (maximum 100)
offsetinteger0Number of results to skip
fromstring--Start date (ISO 8601). Filters by date
tostring--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:

FieldTypeDescription
descriptionstringExpense description (max 10,000 characters)
amountnumberExpense amount

Optional fields:

FieldTypeDescription
categorystringExpense category (max 10,000 characters)
datestringExpense date (ISO 8601). Default: today
vendorstringVendor name (max 10,000 characters)
taxDeductiblebooleanWhether 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:

FieldTypeDescription
namestringClient name (max 10,000 characters)

Optional fields:

FieldTypeDescription
emailstringContact email
phonestringPhone number
taxIdstringTax ID (NIF/CIF/VAT)
addressobjectAddress (see structure below)

address structure:

FieldTypeDescription
streetstringStreet and number
citystringCity
statestringState or province
postalCodestringPostal code
countrystringCountry

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:

FieldTypeDescription
namestringContact person name

Optional fields:

FieldTypeDescription
emailstringContact email
phonestringPhone number
rolestringJob title or role (e.g., "CFO")
isPrimarybooleanWhether 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.

Immutable

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:

FieldTypeDescription
typestringActivity type: call, email, meeting, or task
titlestringDescriptive title for the activity

Optional fields:

FieldTypeDescription
descriptionstringDetailed description
metadataobjectFree-form additional data
Activity types

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:

FieldTypeDescription
contentstringNote 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:

FieldTypeDescription
namestringProduct or service name (max 10,000 characters)
unitPricenumberUnit price

Optional fields:

FieldTypeDescription
descriptionstringDescription (max 10,000 characters)
taxRatenumberTax 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:

ParameterTypeDefaultDescription
limitinteger50Results per page (maximum 100)
offsetinteger0Number of results to skip
statusstring--Filter by status: draft, sent, accepted, rejected, expired
fromstring--Start date (ISO 8601). Filters by issueDate
tostring--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:

FieldTypeDescription
clientNamestringClient name (max 10,000 characters)
itemsarrayQuote line items. Each item: { description, quantity, unitPrice }

Optional fields:

FieldTypeDescription
validUntilstringExpiration date (ISO 8601)
notesstringNotes or terms (max 10,000 characters)
statusstringdraft (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:

ConceptLimit
Items per batch50 maximum
Request size1 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:

ScenarioResult
First request with the keyResource is created normally
Repeated request with the same key (within 24h)Original response is returned, no duplicate created
Same key after 24hTreated 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 POST requests (resource creation)

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
}
}
tip

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:

ParameterTypeDescription
fromstringStart date (ISO 8601)
tostringEnd 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.

CodeMeaningDescription
400Bad RequestA required field is missing, format is incorrect, or a field is not allowed
401UnauthorizedAPI key not provided, invalid, incorrectly formatted, or expired
403ForbiddenAPI key does not have permission to access this resource
404Not FoundThe requested resource does not exist
405Method Not AllowedHTTP method is not supported for this endpoint
413Payload Too LargeRequest body exceeds 1 MB
422Unprocessable EntityData is valid but the server cannot process it (e.g., tax profile not configured)
429Too Many RequestsRate limit of 100 requests per minute exceeded
500Internal Server ErrorServer-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 filters
  • limit: 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.io
  • https://frihet.io
  • https://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:

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection1; mode=block
X-Request-IdUnique 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:

  1. The user authenticates via Firebase Auth
  2. The MCP client sends the Firebase token to /api/oauth/api-key
  3. The server returns a new fri_xxx... key labeled as "MCP OAuth"
  4. 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

  1. Store your API key securely. Never include it in frontend code, public repositories, or logs.
  2. Handle rate limiting. Implement exponential backoff when you receive a 429.
  3. Use pagination. Don't request all records at once; iterate with limit and offset.
  4. Check response codes. Don't assume every request will succeed.
  5. Rotate keys periodically. Create a new key, update your integration, then revoke the old one.
  6. Always use HTTPS. All API requests must go over HTTPS.
  7. Use filters. The status, from, and to parameters reduce the amount of data transferred.
  8. Use PATCH. For partial updates, send only the modified fields.