Factuarea APIDevelopers

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:

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:

KindScopes that consent never grantsWhy
Account administrationaccount: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:writeKeys, team, ownership, tax ID transfers and the tax ID portfolio are decisions of the account holder in their own session.
Provider privilegeaccounts:read, accounts:writeProvisioning, administering and charging the accounts of your clients is never delegated by consent.
Observabilitydevelopers:read, emails:read, integration_events:read, integration_events:writeThey 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 storesstripe_autoinvoicing:read, stripe_autoinvoicing:write, payouts:read, stores:read, stores:write, woocommerce_store:write, shopify_store:writeThey are offered to the integration channel of the account through an API key.
Irreversible, fiscal and labour writesverifactu:write, delivery_notes:gdpr_forget, automations:delete, absences:write, absences:transition, time_entries:write, work_schedules:writeThey 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-server

The 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.

ScopeAllows
contacts:readList and retrieve contacts, in either role.
contacts:writeCreate and update contacts, roles and profiles.
contacts:deleteArchive or delete contacts and remove their roles.
products:readList and retrieve products.
products:writeCreate and update products.
products:deleteDelete products.
price_lists:readList price lists, their items and resolve catalog prices.
price_lists:writeCreate, update and delete price lists and their items.

Sales documents

ScopeAllows
invoices:readList and retrieve invoices.
invoices:writeCreate and update invoices (includes duplicate and corrective).
invoices:deleteDelete invoice drafts.
invoices:sendSend invoice by email to the client.
invoices:voidVoid an issued invoice.
quotes:readList and retrieve quotes.
quotes:writeCreate and update quotes.
quotes:deleteDelete quotes.
quotes:sendSend quote by email.
quotes:transitionAccept, reject or convert quotes.
proformas:readList and retrieve pro-forma invoices.
proformas:writeCreate and update pro-forma invoices.
proformas:deleteDelete pro-forma invoices.
proformas:sendSend pro-forma invoice by email.
proformas:transitionConvert pro-forma invoice to invoice.
delivery_notes:readList and retrieve delivery notes.
delivery_notes:writeCreate and update delivery notes.
delivery_notes:deleteDelete delivery notes.
delivery_notes:transitionMark as delivered/cancelled, sign, convert.
delivery_notes:gdpr_forgetErase signature-audit PII (GDPR Art. 17).

Purchases and recurring

ScopeAllows
purchase_invoices:readList and retrieve expenses.
purchase_invoices:writeCreate and update expenses.
purchase_invoices:deleteDelete expenses.
purchase_invoices:transitionMark as paid, received, accounted.
recurring_invoices:readList and retrieve recurring templates.
recurring_invoices:writeCreate and update recurring templates.
recurring_invoices:deleteDelete recurring templates.
recurring_invoices:transitionPause, resume and emit manually.

Catalogs and export

ScopeAllows
taxes:readRead the Factuarea tax catalog and your company's own taxes.
taxes:writeCreate and update your company's own taxes and set its defaults.
taxes:deleteDelete your company's own taxes.
series:readList invoice numbering series.
series:writeCreate and update invoice numbering series.
pdfs:readDownload PDFs of any document with the matching :read scope.
tax_reports:readRead tax reports (Modelo 303/347, etc.).
tax_reports:writeGenerate tax reports.

VeriFactu & FacturaE

ScopeAllows
verifactu:readRead VeriFactu records, events, certificates and config.
verifactu:writeManage VeriFactu certificates, settings and retries.
facturae:readDownload the FacturaE XML of an invoice and read its FACe submissions.
facturae:writeSubmit invoices to FACe and request submission cancellations.

Webhooks and events

ScopeAllows
webhooks:readList and retrieve webhook endpoints and their deliveries, of a tax ID and of the account.
webhooks:writeCreate, update, rotate, test and ping webhook endpoints, and replay or retry their deliveries.
webhooks:deleteDelete webhook endpoints.
events:readRead the event catalog and individual events.

Integrations and observability

ScopeAllows
stripe_autoinvoicing:readRead Stripe auto-invoicing status, configuration and connected accounts.
stripe_autoinvoicing:writeConfigure Stripe auto-invoicing and connected accounts.
payouts:readRead Stripe payouts and reconciliation status.
integration_events:readInspect received payment-gateway events and typed discard reasons.
integration_events:writeReplay parked integration events.
developers:readInspect the API request log of a tax ID and of the account.
emails:readInspect sent-email status and history.
gocardless_autoinvoicing:readReserved for the unreleased GoCardless integration; no endpoint or tool currently requires it.
gocardless_autoinvoicing:writeReserved for the unreleased GoCardless integration; no endpoint or tool currently requires it.
monei_autoinvoicing:readReserved for the unreleased MONEI integration; no endpoint or tool currently requires it.
monei_autoinvoicing:writeReserved 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.

ScopeAllows
employees:readRead employees and invitation state.
employees:writeCreate, update and manage employees and employee seats.
employees:deletePermanently delete employees.
time_entries:readRead time entries, balances and monthly registers.
time_entries:writeClock, correct and close/reopen time registers.
absences:readRead absence types, policies, requests and balances.
absences:writeCreate and update absence configuration and requests.
absences:transitionApprove, reject and cancel absence requests.
work_schedules:readRead schedules and assignments.
work_schedules:writeCreate, update, assign and archive schedules.
presence:readRead live and daily presence.
holidays:readRead company and regional holidays.
payroll_exports:readRead generated payroll exports.
payroll_exports:writeGenerate 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.

ScopeAllows
projects:readList and retrieve projects, columns and custom field definitions; read the time summary; export tasks; preview time invoices.
projects:writeCreate and update projects, columns and custom field definitions; archive and unarchive; import tasks.
projects:deletePermanently delete projects, columns and custom field definitions.
tasks:readSearch and retrieve tasks, comments, relations, links, attachments, time entries, activity and labels; read the agenda and the running timer.
tasks:writeCreate and update tasks, comments, relations, labels, links, attachments and time entries; change status and assignee; start and stop the timer.
tasks:deletePermanently delete tasks, comments, attachments, time entries and labels.
users:readList the members of the company and retrieve the current user.
notifications:readList the notifications of the key holder.
notifications:writeMark 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).

ScopeAllows
companies:readList 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:writeRegister, edit, activate and deactivate tax IDs of the account, and verify the creation of one.
companies:deleteArchive 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.

ScopeAllowsPlan
account:readList 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:writeCreate, change the tax IDs of, rotate and revoke the API keys of the account, and update personalization.Any plan
account:ownerTransfer the ownership of the account. The person behind the key must be the current owner.Any plan
members:readRead the members of the account, their grants and their module restrictions.Needs the user_management module
members:writeAdd, change and remove members, set and revoke their grants and change their module restrictions.Needs the user_management module
invitations:readList and retrieve the invitations of the account.Needs the user_management module
invitations:writeSend, resend and revoke invitations.Needs the user_management module
nif_transfers:readList the tax ID transfers that the account issued.Needs the user_management module
nif_transfers:writeIssue, accept and revoke a tax ID transfer.Needs the user_management module
api_keys:read, api_keys:write, api_keys:deleteReserved: 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:

ScopeAllows
accounts:readList 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:writeProvision 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/me tells you whether your account holds it, in account.is_provider and account.privileged_scopes. You cannot assign them to yourself: creating a key that lists them without the privilege answers 403 privileged_scope_required and 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

ScopeAllows
*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) and fact_test_ (sandbox). Never mix them across environments.
  • Rotate from the dashboard (or via the self-service account:write endpoints) 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 199 Warning header 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:

codeHTTPCause
missing_api_key401No authentication header sent.
invalid_api_key401The key does not exist, has the wrong format, or the secret doesn't match the stored hash.
api_key_revoked401The secret is right, but the key was revoked.
api_key_expired401The secret is right, but the key passed its expires_at.
ip_not_allowed401The request comes from an IP outside the allowlist of the key.
too_many_auth_failures429Too many failed authentication attempts; back off.
insufficient_scope403The key lacks the scope the endpoint requires. * does not cover the privileged accounts:*.
privileged_scope_required403The 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_required403A 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 :read scopes, and a key that carries only :read scopes needs just view on 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_at for temporary keys (e.g. consultancies, demos).
  • Audit usage from the dashboard: Developers > API Keys > Activity shows 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.

On this page

Need a hand?Contact support