Overview
The API has two addresses. The accounts address, accounts.tributs.ad, tells you which companies you can access. Each company serves its data at its own address, and that data only travels through there.
Any user can create a token, whatever their role. If you work at an advisory firm, a single token reads every one of your clients' companies.
| Base URL | For |
|---|---|
https://accounts.tributs.ad/api/v1 | The companies your token can access |
https://{slug}.tributs.ad/api/v1 | One company's data |
Quickstart
-
Create a token
In Tributs, open your profile → External connections → Create token. Name it after the program that will use it and choose when it expires: 30 days, 90 days or 1 year. The token (
trb_…) is shown once: copy it then. -
Find your companies
Each company comes with its
api_base_url, which is where you read its data.cURLcurl https://accounts.tributs.ad/api/v1/companies \ -H "Authorization: Bearer $TRIBUTS_TOKEN"200 OK · application/json{ "data": [ { "id": "acme.tributs.ad", "name": "Acme Serveis SL", "display_name": "Acme", "role": "advisor", "status": "active", "api_base_url": "https://acme.tributs.ad/api/v1" } ], "next_cursor": null, "has_more": false } -
Read data
Lists come in pages. This example fetches every sales invoice since 1 January.
curl "https://acme.tributs.ad/api/v1/sales_invoices?date_from=2026-01-01&limit=200" \ -H "Authorization: Bearer $TRIBUTS_TOKEN"import os import requests BASE = "https://acme.tributs.ad/api/v1" HEADERS = {"Authorization": f"Bearer {os.environ['TRIBUTS_TOKEN']}"} params = {"date_from": "2026-01-01", "limit": 200} invoices = [] while True: response = requests.get(f"{BASE}/sales_invoices", headers=HEADERS, params=params, timeout=30) response.raise_for_status() page = response.json() invoices += page["data"] if not page["has_more"]: break params["cursor"] = page["next_cursor"]const BASE = "https://acme.tributs.ad/api/v1"; const headers = { Authorization: `Bearer ${process.env.TRIBUTS_TOKEN}` }; const invoices = []; let cursor; do { const url = new URL(`${BASE}/sales_invoices`); url.searchParams.set("date_from", "2026-01-01"); url.searchParams.set("limit", "200"); if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers }); if (!response.ok) { const problem = await response.json().catch(() => ({})); throw new Error(problem.title ?? `HTTP ${response.status}`); } const page = await response.json(); invoices.push(...page.data); cursor = page.has_more ? page.next_cursor : undefined; } while (cursor);
Authentication
Send the token in the Authorization header of every request, with the Bearer scheme. All requests go over HTTPS.
Authorization: Bearer trb_… - The token is opaque:
trb_followed by 40 characters. Tributs stores only a fingerprint (SHA-256), so nobody can see it again. - It always expires: after 30 days, 90 days or 1 year. We email you 7 days before and on the day it expires.
- Renew issues a new key for the same token. The old key stops working within a minute; the name and history are kept.
- You can revoke it at any time. Revocation reaches every company within 60 seconds.
- It acts as you: in each company it has exactly your role and your permissions.
- Up to 10 live tokens per person. Use one token per program.
The company stays in control
Each company's owner sees in the settings who reads it through the API, with which token and when, and can cut access for everyone or for one person.
Pagination and sync
Lists return data, next_cursor and has_more. While has_more is true, call again with the same parameters plus cursor=<next_cursor>. limit goes from 1 to 200 (50 by default, and at most 50 with expand=lines).
Items are ordered by last change, oldest first. To keep a copy in sync, store the updated_at of the last item and pass it as updated_since next time: you get what changed since then. Store items by id, replacing the ones you have, because those from the last second can come back.
A cancelled document comes back with status: "cancelled". Drafts don't appear: only issued documents are returned. Bank transactions are the exception, because they are never issued: they always appear, and a manual one that gets deleted disappears without notice.
Invoices and bank transactions accept sort: total_desc (largest invoices first), issue_date_desc (latest invoices first) and date_desc (latest bank lines first). A cursor is only valid for the sort it was issued with. With include=totals, the first page (the call without cursor) carries totals: how many items match the filters and what they add up to, across every page. Cancelled ones are counted apart and add nothing to the sums.
Resources
Every resource answers GET. The full contract, with every field and parameter, is the OpenAPI 3.1 file each company serves without a token at {api_base_url}/openapi.json. Import it into Postman or generate a client from it.
| Endpoint | Contents | Filters |
|---|---|---|
GET/companies | Companies your token can read, with each one's api_base_url. Call it on accounts.tributs.ad. | — |
GET/contacts | Customers and suppliers, with tax id, country and roles. | role |
GET/sales_invoices | Issued sales invoices, credit notes and daily till summaries. expand=lines adds lines and IGI per rate. | date_fromdate_tostatustypecontact_idmin_totalsortinclude=totalsexpand=lines |
GET/purchase_invoices | Purchase invoices and credit notes, with the deductible IGI actually supported. | date_fromdate_tostatustypecontact_idmin_totalsortinclude=totalsexpand=lines |
GET/journal_lines | General ledger lines: date, account, debit, credit, contact and source document. | date_fromdate_toaccountcontact_iddocument_id |
GET/accounts | Chart of accounts (PGC of Andorra), with number, name and parent account. | — |
GET/bank_transactions | Bank statement lines and their reconciliation status. | date_fromdate_tostatusbank_account_idmin_amountsortinclude=totals |
GET/bank_accounts | The company's bank accounts with two balances: the one in the books (ledger_balance) and the one on the bank statement (statement_balance). | date |
GET/receivables | What each customer owes as of today: open amount, overdue, due within 30 days and advances not yet applied. Largest balance first by default; sort=oldest_due puts the oldest due date first. | min_amountsortinclude=totals |
GET/payables | What is owed to each supplier, with the same fields as receivables. | min_amountsortinclude=totals |
GET/obligations | Tax calendar: model, period, deadline, status and the result of the saved return. GET /obligations/{id} adds the figures box by box. | yearstatusdeadline_fromdeadline_to |
GET/payroll_summary | Payroll totals per month: payslips and people, gross, net, social security (CASS) of the workers and of the company, tax withheld (IRPF and IRNR) and total cost, with salaried staff and partners apart. No names. | month_frommonth_to |
GET/payslips | Payslips, one per person and month: gross, net, social security, tax withheld with its rate, company cost, status and payment date. include=totals adds the sums. | month_frommonth_toemployee_idstatuskindsortinclude=totals |
GET/employees | People on the payroll: name, start and end dates, contract type, job category and tax residence. | statuskind |
GET/reports/profit_and_loss | Profit and loss by heading, with the detail per account. | date_fromdate_toperiodicityperiods |
GET/reports/balance_sheet | Balance sheet by heading, with the detail per account. | dateperiodicityperiods |
GET/reports/trial_balance | Trial balance: opening balance, debit, credit and closing balance per account. | date_fromdate_to |
Paginated lists (every resource except companies, bank_accounts, payroll_summary and the reports) also accept limit and cursor, and updated_since except receivables and payables, which are balances as of today.
To read a single document: GET /sales_invoices/{id}, /purchase_invoices/{id}, /contacts/{id} and /obligations/{id} (by id or by code, such as M900-T2-2026). Invoices always come with their lines, and the obligation with the figures of the saved return (filing): result and breakdown by box for forms 900, 910, 800, 805, 810, 700, 710, 310 and 311. Withholding forms come as totals, without the list of payees.
Not in v1: PDFs and attachments, quotes, delivery notes, subscriptions and projects. For the people on the payroll, the tax id, the social security number, the IBAN and the birth date are never returned.
Example response
{
"data": [
{
"id": "FV-26-00042",
"number": "FV-26-00042",
"type": "invoice",
"status": "unpaid",
"issue_date": "2026-10-02",
"due_date": "2026-11-01",
"contact_id": "Grup Ferrer SL",
"contact_name": "Grup Ferrer SL",
"contact_tax_id": "L712345A",
"currency": "EUR",
"net_amount": 1200.0,
"tax_amount": 54.0,
"total": 1254.0,
"outstanding_amount": 1254.0,
"document_currency": "EUR",
"document_total": 1254.0,
"exchange_rate": 1.0,
"credited_invoice_id": null,
"project_ids": [],
"description": "Consultoria setembre",
"disbursements_amount": 0.0,
"created_at": "2026-10-02T08:14:03Z",
"updated_at": "2026-10-02T08:14:03Z"
}
],
"next_cursor": "WyIyMDI2LTEwLTAyIDEwOjE0OjAzIiwiRlYtMjYtMDAwNDIiXQ",
"has_more": true
} Conventions
- JSON, with field names in English and in
snake_case. - Statuses are stable codes (
paid,unpaid,cancelled…) and are never translated. - Accounting dates are
YYYY-MM-DD(Andorra calendar). Moments are RFC 3339 in UTC. - Amounts are numbers with 2 decimals in EUR, the currency of the books. Foreign-currency documents also carry
document_currency,document_totalandexchange_rate. - Report headings are identified by their
code(Decret 479/2025). The label is the official Catalan text. - The figures of a tax form are identified by their box number (
box); the label is in Catalan, the language of the official form.
Rate limits
60 requests per minute per token and company, in bursts of up to 20. A report counts as 5, and so do bank_accounts, receivables, payables and payroll_summary. Because the limit is per company, a firm can read many companies in parallel; on top of that, each IP address has an overall ceiling of 600 requests per minute.
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests per minute: 60. |
RateLimit-Remaining | Requests you can still make right now. |
RateLimit-Reset | Seconds until the counter is full again. |
RateLimit-Policy | The policy applied: 60;w=60;burst=20. |
Retry-After | With 429 and 503: seconds to wait before retrying. |
Errors
Errors follow RFC 9457 (application/problem+json). The code field is stable, and type links to the matching row of this table.
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
{
"type": "https://tributs.ad/developers#errors-expired-token",
"title": "Access token expired",
"status": 401,
"code": "expired_token",
"instance": "/api/v1/sales_invoices"
} | Status | code | What to do |
|---|---|---|
| 401 | missing_token | Add the header Authorization: Bearer trb_…. |
| 401 | invalid_token | The token does not exist or was copied wrong. Check it or create a new one. |
| 401 | expired_token | The token has expired. Create a new one from your profile. |
| 401 | revoked_token | The token was revoked. Create a new one from your profile. |
| 403 | no_access | You have no access to this company. |
| 403 | permission_denied | Your role in this company cannot read this resource. |
| 403 | api_access_disabled | The owner has turned off API access for the company. |
| 403 | api_access_blocked | The owner has turned off your API access to this company. |
| 403 | company_suspended | The company's subscription is not active. |
| 400 | invalid_parameter | A parameter is not valid. The detail field says which one. |
| 404 | not_found | The resource or document does not exist. |
| 405 | method_not_allowed | The API only accepts GET. |
| 429 | rate_limited | Wait the seconds given in Retry-After. |
| 500 | internal_error | An error on our side. Try again; if it persists, write to us. |
| 503 | maintenance | The company is under maintenance. Try again after the seconds in Retry-After. |
| 503 | auth_unavailable | We could not check the token. Try again in a few seconds. |
Excel and Power BI
In Excel or Power BI, first create a text parameter TributsToken holding the token (Manage parameters). Then open Get data → Blank query → Advanced editor and paste this query, which walks through every page.
let
Token = TributsToken,
Base = "https://acme.tributs.ad/api/v1/sales_invoices",
GetPage = (cursor as nullable text) =>
Json.Document(Web.Contents(Base, [
Query = if cursor = null
then [limit = "200"]
else [limit = "200", cursor = cursor],
Headers = [Authorization = "Bearer " & Token]
])),
Pages = List.Generate(
() => GetPage(null),
each _ <> null,
each if _[has_more] then GetPage(_[next_cursor]) else null
),
Rows = List.Combine(List.Transform(Pages, each _[data])),
Invoices = Table.FromRecords(Rows)
in
Invoices If asked for credentials, choose Anonymous: the token already travels in the header. The fixed base URL with parameters in Query lets the Power BI service refresh the report; there, in the data source credentials, tick “Skip test connection”, because that test runs without the token and gets a 401 error. The token is saved inside the file: use one just for this report and share the file only with people who may see this data.
AI assistants
An AI assistant (ChatGPT, Claude and other programs that support MCP) can read the same data as this API, with the same queries and your permissions. No token is needed: you sign in with your Tributs user.
- In the assistant, add a custom connector with the address
https://mcp.tributs.ad/mcp, including the final/mcp. - A Tributs screen opens: sign in with your user and press Allow.
- Under Settings → Profile → External connections you see the connected app and can withdraw its permission.
- Read-only. Every resource on this page is a query the assistant can run, and new ones reach it on their own.
- Its own limit: 120 requests per minute per connection, with bursts of up to 40, on top of the API limit per company.
- Claude Code, Cursor or your own program that speaks MCP can also sign in with a token: send it as
Authorization: Bearer trb_…to the same address. - What the assistant reads goes through the provider you chose, under its terms.
Security
How we protect access:
- Read-only: no API endpoint changes data.
- Tributs only stores a fingerprint of the token (SHA-256).
- Mandatory expiry, with revocation and renewal from your profile.
- Each company sees who reads it, and its owner can cut access.
- Limits per token and company, and HTTPS on every request.
Best practices
- Keep the token in a secrets manager or an environment variable. Keep it out of code, repositories and apps that run in the browser.
- Create one token per program, with the shortest expiry that works for you.
- Always send it in the
Authorizationheader: URLs end up in browser history and server logs. - If you think it leaked, revoke it from your profile and create a new one. The
trb_prefix makes it easy for secret scanners to find.
To report a vulnerability, write to security@tributs.ad.
Versions
The version is in the URL (/api/v1/). v1 only gains fields: a breaking change goes to v2.