Authentication
API keys with fact_live_ / fact_test_ prefixes, fine-grained scopes, grace-period rotation and IP allowlist.
The Factuarea API authenticates every request with an API key. Keys
are opaque tokens generated in the developer dashboard
(app.factuarea.com/settings/developers/api-keys)
and bound to a specific company. 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 company (production);fact_test_operates on an isolated sandbox company 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. If you lose it, you must rotate.
- DB hash: the backend stores only the bcrypt cost-12 hash of the secret. There's no way to recover it.
Every example in this guide uses a fact_live_ key, but the exact same
request works with a fact_test_ key — just swap the prefix to operate
on sandbox data. 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/clients \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"X-API-Key header
curl https://api.factuarea.com/v1/clients \
-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('clients?limit=10');
$body = json_decode((string) $response->getBody(), true);const res = await fetch('https://api.factuarea.com/v1/clients?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/clients',
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 same scopes gate the access token, and the
rotation policy applies to OAuth client secrets too.
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 raises invalid_scope when creating the key.
Clients and catalog
| Scope | Allows |
|---|---|
clients:read | List and retrieve clients. |
clients:write | Create and update clients. |
clients:delete | Delete clients. |
products:read | List and retrieve products. |
products:write | Create and update products. |
products:delete | Delete products. |
suppliers:read | List and retrieve suppliers. |
suppliers:write | Create and update suppliers. |
suppliers:delete | Delete suppliers. |
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 vendor bills. |
purchase_invoices:write | Create and update vendor bills. |
purchase_invoices:delete | Delete vendor bills. |
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 (global) tax rates catalog. |
taxes:write | Create and update tax rates. |
taxes:delete | Delete tax rates. |
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. |
account:read | Read the authenticated account (GET /v1/account). |
account:write | Manage the account's own API keys (create, rotate, revoke) and update account personalization. |
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 webhook endpoints and deliveries. |
webhooks:write | Create, update, rotate and ping webhook endpoints. |
webhooks:delete | Delete webhook endpoints. |
events:read | Read the event catalog and individual events. |
Managed companies (gestoría)
Fine-grained scopes for the gestoría model, where a master account manages
child sub-companies and their API keys. Reachable only with an API key (no OAuth
consent equivalent); companies:* also requires the gestoría plan module.
| Scope | Allows |
|---|---|
companies:read | List and retrieve managed companies (child sub-accounts). |
companies:write | Create, update, activate and deactivate managed companies. |
companies:delete | Archive managed companies. |
api_keys:read | List and retrieve API keys of managed companies. |
api_keys:write | Create, rotate and revoke API keys of managed companies. |
api_keys:delete | Permanently delete API keys of managed companies. |
Super-scope
| Scope | Allows |
|---|---|
* | Full access — equivalent to having every other scope above. Reserved for owner keys / one-off migrations. Avoid using in production integrations. |
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": "La API key no tiene el scope requerido para esta operación.",
"request_id": "req_01JBVH7..."
}
}Key management
API keys are managed from the developer dashboard
(app.factuarea.com/settings/developers/api-keys),
not via the public API. From there you can create keys, rotate their
secret, revoke them, configure scopes, an optional expires_at, and an
IP allowlist.
The authenticated key's metadata (id, name, prefix, scopes, tier,
last_used_at, expires_at) is readable via GET /v1/account — 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.
- 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 company (tenant) and 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. If the request comes from an IP outside the allowlist, the
response is 401 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 for
insufficient_scope) 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 key was revoked or has expired. Create a new one in the dashboard. |
too_many_auth_failures | 429 | Too many failed authentication attempts; back off. |
insufficient_scope | 403 | The key lacks the scope the endpoint requires. |
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. - 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.