Authentication
API keys with fact_live_ / fact_test_ prefixes, fine-grained scopes, the privileged accounts:* scopes, what OAuth consent reaches, grace-period rotation and IP allowlist.
The Factuarea API authenticates every request with an API key. Keys
are opaque tokens issued by an owner or an admin of the account in the
developer dashboard
(app.factuarea.com/settings/developers/api-keys),
or through the self-service v1 endpoints. A key belongs to
an account, never to one tax ID. Its reach is its scope: the tax IDs
assigned to it, which can be tax IDs of the account and tax IDs of another
account that lets you reach them. Every tax ID route
names the one it operates on in its own path (/v1/companies/{company}/…), and
every account route names your account (/v1/accounts/{account}/…).
GET /v1/me enumerates the scope — one entry per tax ID, with the opaque id
the path expects and its tax_id — so a credential never has to guess which
tax IDs it reaches. Every request to https://api.factuarea.com/v1/* must
include a valid key in one of the two supported formats.
API key format
fact_live_<24 alphanumeric characters>
fact_test_<24 alphanumeric characters>Example:
fact_live_8KqW3pXnR2VbY7TcA9eFmN5z
fact_test_3pXnR2VbY7TcA9eFmN5z8KqW- Prefix: determines the environment.
fact_live_operates on your real tax IDs (production);fact_test_reaches only the isolated sandbox company of your account, with external effects (VeriFactu → AEAT, FACe, emails, webhooks) switched off. The prefix lets you identify the environment without decoding the key. See Test mode & sandbox. - Secret: 24 base62 characters → ~143 bits of entropy. Shown only once at creation, in the dashboard or in the API response. If you lose it, you must rotate.
- Storage: the backend stores only a keyed hash (HMAC-SHA256) of the secret, never the secret. There's no way to recover it.
Every example in this guide uses a fact_live_ key. A fact_test_ key
works on the same routes, but only on the sandbox company of your account:
take its id from GET /v1/me and use it as {company}. Build and validate
your integration in test first. See Test mode & sandbox.
Sending the key on each request
The API accepts two equivalent formats. Pick the one that fits your client:
Authorization Bearer (recommended)
curl https://api.factuarea.com/v1/companies/{company}/contacts \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"X-API-Key header
curl https://api.factuarea.com/v1/companies/{company}/contacts \
-H "X-API-Key: fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"Send only one of the two headers. If both are present, the
Authorization: Bearer header takes precedence.
Examples per language
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.factuarea.com/v1/',
'headers' => [
'Authorization' => 'Bearer ' . getenv('FACTUAREA_API_KEY'),
'Accept' => 'application/json',
],
]);
$response = $client->get('companies/{company}/contacts?limit=10');
$body = json_decode((string) $response->getBody(), true);const res = await fetch('https://api.factuarea.com/v1/companies/{company}/contacts?limit=10', {
headers: {
Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`,
Accept: 'application/json',
},
});
const data = await res.json();import os
import requests
resp = requests.get(
'https://api.factuarea.com/v1/companies/{company}/contacts',
params={'limit': 10},
headers={
'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}",
'Accept': 'application/json',
},
)
resp.raise_for_status()
data = resp.json()OAuth 2.1
For agent integrations and third-party apps that act on behalf of a
Factuarea user, the API also supports the OAuth 2.1 authorization-code
flow with PKCE (code_challenge_method=S256) as an alternative to a
static API key. The OAuth consent catalog has 73 simple dotted scopes plus
the factuarea.read, factuarea.write and factuarea.full macros; they are
mapped to the fine-grained colon scopes that gate each endpoint and MCP tool.
The rotation policy applies to OAuth client secrets too.
The consent catalog includes price_lists.read/write,
facturae.read/write and tax_reports.read/write. The screen offers only the
scopes requested by the client, never pre-selects sensitive scopes, and the
issued token contains exactly the subset the user approved. One scope of the
catalog, payroll_exports.write, is not offered, because no tool requires the
scope it translates to.
What OAuth consent reaches
Consent grants the business data of a tax ID and never the administration of the account. A fine-grained scope is out of reach when the catalog has no dotted scope that translates to it, and these ones are excluded on purpose, so that an app of a third party can never obtain them whatever the user approves:
| Kind | Scopes that consent never grants | Why |
|---|---|---|
| Account administration | account:write, account:owner, members:read, members:write, invitations:read, invitations:write, nif_transfers:read, nif_transfers:write, companies:read, companies:write, companies:delete, api_keys:read, api_keys:write | Keys, team, ownership, tax ID transfers and the tax ID portfolio are decisions of the account holder in their own session. |
| Provider privilege | accounts:read, accounts:write | Provisioning, administering and charging the accounts of your clients is never delegated by consent. |
| Observability | developers:read, emails:read, integration_events:read, integration_events:write | They expose the technical activity of the credentials, the email history and the events of the payment gateways of the account, and replaying one of those events can issue a real invoice. |
| Payments and stores | stripe_autoinvoicing:read, stripe_autoinvoicing:write, payouts:read, stores:read, stores:write, woocommerce_store:write, shopify_store:write | They are offered to the integration channel of the account through an API key. |
| Irreversible, fiscal and labour writes | verifactu:write, delivery_notes:gdpr_forget, automations:delete, absences:write, absences:transition, time_entries:write, work_schedules:write | They change fiscal records, erase data or act in the name of labour managers, so they stay with first-party controls or with the holder through an API key. |
Everything else that the catalog translates is reachable: contacts, the catalog,
sales documents, purchases and recurring invoices, tax reports, FacturaE,
VeriFactu reads, webhooks, automations and workforce reads, and account:read
through profile.read. Some operations stay out of OAuth even though their scope
is reachable, such as the webhooks and the request logs of the account: MCP
tools shows the channel of each tool. Use an API key for every
excluded scope, also with the MCP server. MCP scopes lists the
consent catalog.
Discovery metadata (RFC 8414) is published at
/.well-known/oauth-authorization-server, so OAuth clients can resolve
the authorization and token endpoints automatically:
curl https://api.factuarea.com/.well-known/oauth-authorization-serverThe OAuth2 security scheme — including the authorization and token URLs
and the full scope list — is described in the
API Reference.
Scopes
Each API key is created with one or more scopes that limit which
endpoints it can invoke. Scopes are strings of the form
<resource>:<action>. The catalog is closed: any scope outside the
listed set fails validation with
422 invalid_param_value when creating the key.
The valid catalog is append-only for existing credentials, while the subset
assignable to a new key follows the currently released API surface. The four
reserved GoCardless/MONEI scopes below remain recognizable for historical
compatibility, but are omitted from CreateApiKeyV1Request and cannot be
assigned until those providers are released. Plan and sandbox restrictions are
then enforced separately with scope_not_allowed_by_plan and
scope_not_allowed_in_sandbox: a scope is granted only if the plan of every
tax ID in the scope of the key allows it.
Contacts and catalog
A customer and a supplier are two roles of the same identity — the
contact — so a single trio of scopes covers both. The former clients:* and
suppliers:* scopes are retired and no longer assignable.
| Scope | Allows |
|---|---|
contacts:read | List and retrieve contacts, in either role. |
contacts:write | Create and update contacts, roles and profiles. |
contacts:delete | Archive or delete contacts and remove their roles. |
products:read | List and retrieve products. |
products:write | Create and update products. |
products:delete | Delete products. |
price_lists:read | List price lists, their items and resolve catalog prices. |
price_lists:write | Create, update and delete price lists and their items. |
Sales documents
| Scope | Allows |
|---|---|
invoices:read | List and retrieve invoices. |
invoices:write | Create and update invoices (includes duplicate and corrective). |
invoices:delete | Delete invoice drafts. |
invoices:send | Send invoice by email to the client. |
invoices:void | Void an issued invoice. |
quotes:read | List and retrieve quotes. |
quotes:write | Create and update quotes. |
quotes:delete | Delete quotes. |
quotes:send | Send quote by email. |
quotes:transition | Accept, reject or convert quotes. |
proformas:read | List and retrieve pro-forma invoices. |
proformas:write | Create and update pro-forma invoices. |
proformas:delete | Delete pro-forma invoices. |
proformas:send | Send pro-forma invoice by email. |
proformas:transition | Convert pro-forma invoice to invoice. |
delivery_notes:read | List and retrieve delivery notes. |
delivery_notes:write | Create and update delivery notes. |
delivery_notes:delete | Delete delivery notes. |
delivery_notes:transition | Mark as delivered/cancelled, sign, convert. |
delivery_notes:gdpr_forget | Erase signature-audit PII (GDPR Art. 17). |
Purchases and recurring
| Scope | Allows |
|---|---|
purchase_invoices:read | List and retrieve expenses. |
purchase_invoices:write | Create and update expenses. |
purchase_invoices:delete | Delete expenses. |
purchase_invoices:transition | Mark as paid, received, accounted. |
recurring_invoices:read | List and retrieve recurring templates. |
recurring_invoices:write | Create and update recurring templates. |
recurring_invoices:delete | Delete recurring templates. |
recurring_invoices:transition | Pause, resume and emit manually. |
Catalogs and export
| Scope | Allows |
|---|---|
taxes:read | Read the Factuarea tax catalog and your company's own taxes. |
taxes:write | Create and update your company's own taxes and set its defaults. |
taxes:delete | Delete your company's own taxes. |
series:read | List invoice numbering series. |
series:write | Create and update invoice numbering series. |
pdfs:read | Download PDFs of any document with the matching :read scope. |
tax_reports:read | Read tax reports (Modelo 303/347, etc.). |
tax_reports:write | Generate tax reports. |
VeriFactu & FacturaE
| Scope | Allows |
|---|---|
verifactu:read | Read VeriFactu records, events, certificates and config. |
verifactu:write | Manage VeriFactu certificates, settings and retries. |
facturae:read | Download the FacturaE XML of an invoice and read its FACe submissions. |
facturae:write | Submit invoices to FACe and request submission cancellations. |
Webhooks and events
| Scope | Allows |
|---|---|
webhooks:read | List and retrieve webhook endpoints and their deliveries, of a tax ID and of the account. |
webhooks:write | Create, update, rotate, test and ping webhook endpoints, and replay or retry their deliveries. |
webhooks:delete | Delete webhook endpoints. |
events:read | Read the event catalog and individual events. |
Integrations and observability
| Scope | Allows |
|---|---|
stripe_autoinvoicing:read | Read Stripe auto-invoicing status, configuration and connected accounts. |
stripe_autoinvoicing:write | Configure Stripe auto-invoicing and connected accounts. |
payouts:read | Read Stripe payouts and reconciliation status. |
integration_events:read | Inspect received payment-gateway events and typed discard reasons. |
integration_events:write | Replay parked integration events. |
developers:read | Inspect the API request log of a tax ID and of the account. |
emails:read | Inspect sent-email status and history. |
gocardless_autoinvoicing:read | Reserved for the unreleased GoCardless integration; no endpoint or tool currently requires it. |
gocardless_autoinvoicing:write | Reserved for the unreleased GoCardless integration; no endpoint or tool currently requires it. |
monei_autoinvoicing:read | Reserved for the unreleased MONEI integration; no endpoint or tool currently requires it. |
monei_autoinvoicing:write | Reserved for the unreleased MONEI integration; no endpoint or tool currently requires it. |
Workforce (control horario)
These scopes require the control_horario plan module. Some read scopes are
OAuth-grantable; privileged writes and transitions remain API-key-only.
| Scope | Allows |
|---|---|
employees:read | Read employees and invitation state. |
employees:write | Create, update and manage employees and employee seats. |
employees:delete | Permanently delete employees. |
time_entries:read | Read time entries, balances and monthly registers. |
time_entries:write | Clock, correct and close/reopen time registers. |
absences:read | Read absence types, policies, requests and balances. |
absences:write | Create and update absence configuration and requests. |
absences:transition | Approve, reject and cancel absence requests. |
work_schedules:read | Read schedules and assignments. |
work_schedules:write | Create, update, assign and archive schedules. |
presence:read | Read live and daily presence. |
holidays:read | Read company and regional holidays. |
payroll_exports:read | Read generated payroll exports. |
payroll_exports:write | Generate payroll exports. |
Tasks & projects
projects:* and tasks:* require the tasks module, which every plan includes;
users:read and notifications:* are cross-cutting. The projects:delete and tasks:delete scopes are
API-key-only: an OAuth consent never grants them. Invoicing the hours of a
project also needs invoices:write. See Projects and tasks.
| Scope | Allows |
|---|---|
projects:read | List and retrieve projects, columns and custom field definitions; read the time summary; export tasks; preview time invoices. |
projects:write | Create and update projects, columns and custom field definitions; archive and unarchive; import tasks. |
projects:delete | Permanently delete projects, columns and custom field definitions. |
tasks:read | Search and retrieve tasks, comments, relations, links, attachments, time entries, activity and labels; read the agenda and the running timer. |
tasks:write | Create and update tasks, comments, relations, labels, links, attachments and time entries; change status and assignee; start and stop the timer. |
tasks:delete | Permanently delete tasks, comments, attachments, time entries and labels. |
users:read | List the members of the company and retrieve the current user. |
notifications:read | List the notifications of the key holder. |
notifications:write | Mark notifications as read. |
Tax ID portfolio
Fine-grained scopes for the tax IDs that your account holds. They work on
/v1/accounts/{account}/companies… and need an API key: consent never grants
them. Any account, on any plan, lists and reads its own tax IDs, so companies:*
has no plan gate. What the plan gates is adding a tax ID that the account pays
for, and it does so inside the operation (additional tax IDs).
| Scope | Allows |
|---|---|
companies:read | List and retrieve the tax IDs of the account, their statistics, creation status and issuing readiness, the charge preview of an additional tax ID and the consolidated workforce compliance overview. |
companies:write | Register, edit, activate and deactivate tax IDs of the account, and verify the creation of one. |
companies:delete | Archive a tax ID of the account. |
Tax ID portfolio describes the operations.
Account administration
Scopes for what the account itself holds: its keys, its team and its ownership.
They need an API key, except account:read, which OAuth consent grants through
profile.read. They are not privileged, so the super-scope * does cover them.
| Scope | Allows | Plan |
|---|---|---|
account:read | List and retrieve the API keys of the account, and read its usage, its billing, the templates of its personalization and the AEAT census verification of a tax ID. | Any plan |
account:write | Create, change the tax IDs of, rotate and revoke the API keys of the account, and update personalization. | Any plan |
account:owner | Transfer the ownership of the account. The person behind the key must be the current owner. | Any plan |
members:read | Read the members of the account, their grants and their module restrictions. | Needs the user_management module |
members:write | Add, change and remove members, set and revoke their grants and change their module restrictions. | Needs the user_management module |
invitations:read | List and retrieve the invitations of the account. | Needs the user_management module |
invitations:write | Send, resend and revoke invitations. | Needs the user_management module |
nif_transfers:read | List the tax ID transfers that the account issued. | Needs the user_management module |
nif_transfers:write | Issue, accept and revoke a tax ID transfer. | Needs the user_management module |
api_keys:read, api_keys:write, api_keys:delete | Reserved: no endpoint or tool requires them. | Any plan |
GET /v1/me needs no scope: any valid credential can introspect itself. On a
plan without the user_management module the members:*, invitations:* and
nif_transfers:* scopes cannot be assigned to a key, and GET /v1/me lists only
the effective ones. The empresario and enterprise plans include the module.
API keys are not managed with api_keys:*. The six endpoints under
/v1/accounts/{account}/api-keys are governed by account:read (list and
retrieve) and account:write (create, change the tax IDs of, rotate the secret
and revoke) — see API keys and
scoped keys. Accounts and tax IDs
describes the team and the transfers.
Privileged scopes
Two scopes are privileged: accounts:read and accounts:write. They let an
account provision, administer and charge accounts for its clients, which
managed accounts describes. They differ from every
other scope:
| Scope | Allows |
|---|---|
accounts:read | List and retrieve the managed accounts of your account, read their provisioning usage and the preview of a plan change, and download the template of a bulk import. |
accounts:write | Provision a managed account, change its plan and its level, reissue its voucher, end its management, and preview and apply a bulk import. |
- Factuarea grants them, account by account. Only Factuarea can: the API has
no operation that grants the privilege.
GET /v1/metells you whether your account holds it, inaccount.is_providerandaccount.privileged_scopes. You cannot assign them to yourself: creating a key that lists them without the privilege answers403 privileged_scope_requiredand creates nothing. - They are checked on every request. The key must list the scope, your
account must hold the privilege today and the person who issued the key must
still be an owner or an admin of your account. If any of the three fails the
answer is
403 privileged_scope_required, and both causes answer alike. Nothing comes from a cache: if we revoke the privilege, or that person stops being an owner or an admin, the same key stops working on its next request. - They do not come through OAuth. Consent never grants them: use an API key, also with the MCP server.
- No plan gates them. The gate is the privilege, not the plan.
*does not cover them. See the super-scope.
Super-scope
| Scope | Allows |
|---|---|
* | Full access — equivalent to having every other scope above, except the privileged accounts:read and accounts:write. Reserved for one-off migrations. Avoid using in production integrations. |
* does not cover accounts:*. A key that holds only * answers
403 insufficient_scope on a privileged
operation, however broad it looks. * does cover the rest of the account:
the keys, the team, the ownership and the transfers of tax IDs. A key with *
needs operate on every tax ID it reaches, and with the privilege you can list
* and accounts:* together.
If a request uses an endpoint that requires a scope not granted to the
key, the response is 403 with type: authorization_error and
code: insufficient_scope.
{
"error": {
"type": "authorization_error",
"code": "insufficient_scope",
"message": "Esta operación requiere el scope 'account:write'. Tu API key solo tiene los scopes: ninguno.",
"doc_url": "https://docs.factuarea.com/guides/errors#insufficient_scope",
"request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV"
}
}Key management
API keys can be managed from the developer dashboard (app.factuarea.com/settings/developers/api-keys) or through the self-service v1 endpoints. Both surfaces let you create, rotate and revoke keys and set the IP allowlist when you create one; the dashboard also edits it afterwards. The dashboard is for the owners and the admins of the account, and the keys that you issue never reach more than you do: who can issue a key explains it.
The authenticated key's metadata (id, name, prefix, scopes, tier,
environment, last_used_at, expires_at) is readable via GET /v1/me,
alongside the tax IDs the key reaches — but the secret is never
returned.
There is no endpoint to "view" the secret. It is shown only once at creation. If you lose the value you must rotate the key in the dashboard and redeploy the new secret. This is deliberate: it minimizes the exposure window.
Rotation policy
API keys and OAuth client secrets are long-lived credentials and must be rotated on a schedule and immediately after any suspected leak.
- Prefixes are the source of truth for the environment:
fact_live_(production) andfact_test_(sandbox). Never mix them across environments. - Rotate from the dashboard (or via the self-service
account:writeendpoints) to issue a brand-new secret. The new secret is returned once — store it immediately, it is never shown again. - Grace window (dual-secret). After a rotation the previous secret
keeps working for a 24-hour grace window, so you can roll out the
new secret with zero downtime. During that window both the new and the
previous secret are accepted; once the window expires the previous
secret is rejected and purged. A request still using the previous
secret receives a
199Warningheader telling you how many hours remain before it stops working. - When to rotate: on a regular schedule (e.g. every 90 days), whenever a teammate with access leaves, and immediately if a secret is ever exposed in logs, source control or a public client. Rotation leaves the previous secret valid for the 24-hour grace window, so to cut an exposed secret off at once, revoke the key instead (revocation is immediate) and issue a new one.
- Revoke to invalidate a key permanently. Any subsequent request with
it fails with
401. Revocation has no grace window — it is instant and irreversible.
Secrets are bound to a single account and reach only the tax IDs in their scope; they must never be embedded in browsers, mobile apps or any public client — keep them server-side only.
IP allowlist
Each API key can be restricted to a list of IPs or CIDR ranges, from the
dashboard or with ip_allowlist when you create the key. If the request comes
from an IP outside the allowlist, the response is 401 with the code
ip_not_allowed and the incident is recorded in the
audit log. Leave the allowlist empty to allow any IP.
Authentication errors
Failures related to the API key respond with HTTP 401 (or 403 when the key
authenticates but is not enough) and the standard error envelope. The code
field distinguishes the case:
code | HTTP | Cause |
|---|---|---|
missing_api_key | 401 | No authentication header sent. |
invalid_api_key | 401 | The key does not exist, has the wrong format, or the secret doesn't match the stored hash. |
api_key_revoked | 401 | The secret is right, but the key was revoked. |
api_key_expired | 401 | The secret is right, but the key passed its expires_at. |
ip_not_allowed | 401 | The request comes from an IP outside the allowlist of the key. |
too_many_auth_failures | 429 | Too many failed authentication attempts; back off. |
insufficient_scope | 403 | The key lacks the scope the endpoint requires. * does not cover the privileged accounts:*. |
privileged_scope_required | 403 | The operation needs a privileged scope and the account does not hold the privilege, or the person who issued the key is no longer an owner or an admin. |
live_credential_required | 403 | A fact_test_ key tries to change the account: its keys, team, ownership, transfers or tax ID portfolio. |
Every response includes a unique request_id (also in the
X-Request-Id header) you can pass to support when investigating.
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "La API key proporcionada no es válida.",
"request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV",
"doc_url": "https://docs.factuarea.com/guides/errors#invalid_api_key"
}
}Best practices
- Never commit API keys to repositories — use environment variables or a secret manager (AWS Secrets Manager, Doppler, 1Password Service Accounts).
- Create one key per integration: makes rotating and auditing access easier without affecting the rest.
- Limit scopes to the minimum required. An export script only needs
specific
:readscopes, and a key that carries only:readscopes needs justviewon its tax IDs. - Give each key only the tax IDs it needs, with
scoped_company_ids(scoped keys). - Enable IP allowlist for server-to-server integrations with stable IPs.
- Configure
expires_atfor temporary keys (e.g. consultancies, demos). - Audit usage from the dashboard:
Developers > API Keys > Activityshows IPs, paths and errors per key.
Credentials for integrations
Store REST and MCP operations require an API key with the store scopes. OAuth consent does not grant these scopes. Authorize the provider connection separately: a Factuarea key is not a WooCommerce or Shopify credential. MCP authentication also requires a client compatible with revision 2026-07-28.