Frihet Products API
Product and service catalog with pricing, tax rates, and SKUs.
Product and service catalog with pricing, tax rates, and SKUs.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/frihet-io:frihet-io-products-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Frihet Products API
version: '2026-03-18'
description: 'The Frihet REST API provides programmatic access to your business data
in Frihet, the AI-native business management platform for modern businesses. ## Authentication
All API requests require an API key passed via the `X-API-Key` header. Generate keys from **Settings > API Keys** in the Frihet app. ```
X-API-Key: fri_your_api_key_here
```
### API-key scopes
A key without scopes (no list, or an empty one) is unrestricted. A key with a
non-empty scope list gets only what the list grants. On such a key, every
request other than `GET` and `HEAD` needs the `write` scope (or `*`), except
the read-only `POST /v1/gestoria/aging` and the POS and purchase-order routes,
which check their own `pos:` and `purchase-orders:` scopes. E-invoicing endpoints need an `einvoice:` scope,
and the sends (`einvoice/export`, `face/submit`, `ticketbai/submit`) also need
`write` or `einvoice:write`. A request without the scope it needs gets
`403 Forbidden` with a `detail` naming that scope. The refusal happens before
the idempotency gate, so it does not reserve the `Idempotency-Key`. `GET /v1/permissions/me` reports `denied.write`, `denied.einvoice` and
`denied.einvoiceSend` for the calling key. Inventory (`/v1/inventory/*`) and purchase-order (`/v1/purchase-orders/*`) routes are
not released yet: after the scope checks they answer `503` with code
`INVENTORY_AUTHORITY_NOT_RELEASED`, before the idempotency gate. ## Rate Limiting
The API enforces a rate limit of **100 requests per minute** per API key. When exceeded, the API returns `429 Too Many Requests` with an integer
delta-seconds `Retry-After` header and matching `retryAfter` JSON field. Every authenticated response includes rate limit headers:
| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Maximum requests per window (100) |
| `X-RateLimit-Remaining` | Remaining requests in current window |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets |
| `Retry-After` | Integer seconds until the window resets (429 responses) |
## Idempotency
Most POST requests accept an `Idempotency-Key` header to prevent duplicate
resource creation from network retries. For protected routes, the key is
reserved against the operation *before* the request is executed, and it stays
reserved for 24 hours unless released by an explicit pre-write refusal. ```
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
```
Keys must be at most 64 characters. We recommend UUID v4. A retry of the same operation gets one of three answers:
| Situation | Response |
|---|---|
| The first request finished | The **original status and body**, verbatim, with `X-Idempotent-Replayed: true` |
| The first request is still running, or its outcome could not be recorded | `409 IDEMPOTENCY_REQUEST_IN_PROGRESS` |
| The key is live but was used for a **different** operation | `409 IDEMPOTENCY_KEY_REUSED` — the second operation is not executed |
The replayed status is the **stored** one. A `201` replays as `201`, not `200`. This includes stored error responses: a request that answered `4xx` or `5xx`
replays that same status and body, it does not re-execute — except explicit
pre-mutation refusals on `POST /v1/pos/cash-sessions/{id}/close` (`503` when
the workspace POS money-authority grant is not enabled and `403 POS_INACTIVE`
before the POS app is activated), which release the reservation instead of
caching the refusal for 24 hours. ### When a retry is refused
A `409 IDEMPOTENCY_REQUEST_IN_PROGRESS` after the first request already
returned means the outcome could not be recorded — the response was too large
or unserializable, or the bookkeeping write failed. The API refuses rather than
execute a second time, because for a fiscal document a duplicate is not
recoverable. **Reconcile; do not simply retry with a fresh key.** Read the resource back
(`GET` the collection, or look for the document the operation would have
created) and decide from its actual state.'
contact:
name: Frihet API Support
url: https://docs.frihet.io/desarrolladores/api-rest
email: api@frihet.io
license:
name: MIT
url: https://opensource.org/licenses/MIT
x-logo:
url: https://frihet.io/logos/frihet-logo.svg
servers:
- url: https://api.frihet.io
description: Production
security:
- ApiKeyAuth: []
tags:
- name: Products
description: Product and service catalog with pricing, tax rates, and SKUs.
paths:
/v1/products:
get:
operationId: listProducts
summary: List products
description: Retrieve a paginated list of products and services with optional search.
tags:
- Products
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Cursor'
- $ref: '#/components/parameters/From'
- $ref: '#/components/parameters/To'
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/Fields'
- $ref: '#/components/parameters/FilterIsActive'
responses:
'200':
description: A paginated list of products
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/PaginatedResponse'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Product'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
post:
operationId: createProduct
summary: Create a product
description: Add a new product or service to your catalog.
tags:
- Products
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductCreate'
responses:
'201':
description: Product created
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/WriteScopeForbidden'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
/v1/products/{productId}:
get:
operationId: getProduct
summary: Get a product
description: Retrieve a single product by ID.
tags:
- Products
parameters:
- $ref: '#/components/parameters/ProductId'
- $ref: '#/components/parameters/Fields'
responses:
'200':
description: Product details
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
put:
operationId: updateProduct
summary: Full update a product
description: Replace all fields on an existing product or service.
tags:
- Products
parameters:
- $ref: '#/components/parameters/ProductId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductCreate'
responses:
'200':
description: Product updated
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/WriteScopeForbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
patch:
operationId: patchProduct
summary: Partial update a product
description: Update only the provided fields on an existing product or service.
tags:
- Products
parameters:
- $ref: '#/components/parameters/ProductId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProductCreate'
responses:
'200':
description: Product updated
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/WriteScopeForbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
delete:
operationId: deleteProduct
summary: Delete a product
description: Permanently delete a product from your catalog.
tags:
- Products
parameters:
- $ref: '#/components/parameters/ProductId'
responses:
'204':
description: Product deleted
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/WriteScopeForbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/InternalError'
components:
parameters:
Search:
name: q
in: query
description: 'Full-text search query. Searches across relevant fields per resource type
(e.g., clientName, documentNumber for invoices; name, email for clients).
Case-insensitive.
'
schema:
type: string
example: acme
Offset:
name: offset
in: query
description: Number of items to skip for pagination (max 10000)
schema:
type: integer
minimum: 0
maximum: 10000
default: 0
ProductId:
name: productId
in: path
required: true
description: Product unique identifier
schema:
type: string
To:
name: to
in: query
description: End date filter in ISO format (YYYY-MM-DD)
schema:
type: string
format: date
example: '2026-12-31'
From:
name: from
in: query
description: Start date filter in ISO format (YYYY-MM-DD)
schema:
type: string
format: date
example: '2026-01-01'
Limit:
name: limit
in: query
description: Number of items to return (default 50, max 100)
schema:
type: integer
minimum: 1
maximum: 100
default: 50
FilterIsActive:
name: isActive
in: query
description: Filter products by active status
schema:
type: boolean
example: true
Cursor:
name: cursor
in: query
description: 'Cursor for cursor-based pagination. Use the `nextCursor` value from
a previous response to fetch the next page. When provided, `offset`
is ignored. The cursor is a base64url-encoded JSON string.
'
schema:
type: string
example: eyJpc3N1ZURhdGUiOiIyMDI2LTAzLTIwIiwiX19pZCI6ImFiYzEyMyJ9
Fields:
name: fields
in: query
description: 'Comma-separated list of fields to include in the response.
The `id` field is always included. Example: `fields=id,name,total`
'
schema:
type: string
example: id,name,total
schemas:
Error:
type: object
required:
- error
properties:
error:
type: string
description: Human-readable error message
code:
type: string
description: Optional machine-readable code, including MISSING_FISCAL_INFO, AEAT_QR_UNAVAILABLE (422) and AEAT_RECORD_IRREPRODUCIBLE (409) on invoice PDF.
message:
type: string
description: Optional explanatory message accompanying the error code.
details:
type: array
description: Validation error details (Zod issues). Only present on 400 validation errors.
items:
type: object
properties:
code:
type: string
message:
type: string
path:
type: array
items:
type: string
meta:
type: object
properties:
requestId:
type: string
description: Unique request identifier for tracing
Product:
allOf:
- type: object
properties:
id:
type: string
description: Unique identifier
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
- $ref: '#/components/schemas/ProductCreate'
PaginatedResponse:
type: object
properties:
data:
type: array
items: {}
total:
type: integer
description: Total number of items matching the query
limit:
type: integer
description: Number of items per page
offset:
type: integer
description: Current offset
nextCursor:
type: string
description: 'Cursor for fetching the next page of results. Only present when there
are more results available. Pass this value as the `cursor` query parameter
in the next request. Base64url-encoded.
'
example: eyJpc3N1ZURhdGUiOiIyMDI2LTAzLTIwIiwiX19pZCI6ImFiYzEyMyJ9
truncated:
type: boolean
description: 'Only present (and `true`) when the underlying search query
saturated the internal 500-document cap. `total` therefore reports
the size of the first 500 matches, not the full match count.
Narrow the search query (taxId / fiscal id, exact email) to ensure
you see every result.
'
example: true
ProductCreate:
type: object
required:
- name
- unitPrice
properties:
name:
type: string
description: Product or service name
example: Consulting Hour
unitPrice:
type: number
description: Unit price
example: 120
description:
type: string
description: Product description
example: One hour of strategic consulting
taxRate:
type: number
minimum: 0
maximum: 100
description: Default tax rate percentage
example: 21
responses:
InternalError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error: Internal server error
meta:
requestId: 550e8400-e29b-41d4-a716-446655440000
BadRequest:
description: Bad request - invalid parameters or request body
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error: Validation error
details:
- code: invalid_type
message: Required
path:
- clientName
meta:
requestId: 550e8400-e29b-41d4-a716-446655440000
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error: Resource not found
meta:
requestId: 550e8400-e29b-41d4-a716-446655440000
Unauthorized:
description: Authentication required or invalid API key
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error: Invalid or expired API key
meta:
requestId: 550e8400-e29b-41d4-a716-446655440000
RateLimited:
description: Rate limit exceeded (100 requests per minute)
headers:
Retry-After:
description: Integer seconds until the current rate-limit window resets
schema:
type: integer
minimum: 1
example: 60
X-RateLimit-Reset:
$ref: '#/components/headers/X-RateLimit-Reset'
content:
application/json:
schema:
type: object
properties:
error:
type: string
example: Rate limit exceeded
message:
type: string
example: Maximum 100 requests per minute
retryAfter:
type: integer
description: Seconds until rate limit resets
example: 60
meta:
type: object
properties:
requestId:
type: string
WriteScopeForbidden:
description: Write scope required to mutate, send or submit. The API key must include the `write` scope (or `einvoice:write` for e-invoice sends) or be unrestricted.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
example:
error: Forbidden
detail: This API key does not have the write scope required to create, update, delete, send or submit. E-invoice sends also accept einvoice:write. Use an API key with write access.
meta:
requestId: 550e8400-e29b-41d4-a716-446655440000
headers:
X-RateLimit-Reset:
description: Unix timestamp (seconds) when the rate limit window resets
schema:
type: integer
example: 1742310060
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: 'API key for authentication. Keys start with `fri_` prefix.
Generate keys from Settings > API Keys in the Frihet app.
'