API keys (self-service)
Issue, list, narrow, rotate and revoke the API keys of your account over the v1 API — who can issue them, live and test environments, the scopes you can assign and the level ceiling per tax ID.
Beyond the developer dashboard, Factuarea exposes the full lifecycle of your
API keys over the public v1 API, so you can provision and rotate credentials
programmatically. An API key belongs to the account, never to one of its
tax IDs, so keys live on the account axis: six endpoints under
/v1/accounts/{account}/api-keys cover listing, creating, retrieving,
narrowing, rotating the secret and revoking.
| Operation | Endpoint | Scope |
|---|---|---|
| List keys | GET /v1/accounts/{account}/api-keys | account:read |
| Create a key | POST /v1/accounts/{account}/api-keys | account:write |
| Retrieve a key | GET /v1/accounts/{account}/api-keys/{api_key} | account:read |
| Change the tax IDs of a key | PATCH /v1/accounts/{account}/api-keys/{api_key}/nif-scope | account:write |
| Rotate the secret | POST /v1/accounts/{account}/api-keys/{api_key}/rotate-secret | account:write |
| Revoke a key | POST /v1/accounts/{account}/api-keys/{api_key}/revoke | account:write |
{account} is the opaque id of the account the credential belongs to — read
it from account.id in GET /v1/me. It must be your own account: any
other value answers the same 404 account_not_found.
{api_key} is the key's id — an opaque UUID v7, not its prefix or
secret. See the full schemas in the
API Reference.
Who can issue a key
A key is always issued by a person, and what it can reach is measured against that person. Keys are issued in two places:
| Where | Who can | What the new key gets |
|---|---|---|
| The web app, in Settings → Developers → API Keys | An owner or an admin of the account that owns the tax ID you have selected. A member, an external advisor (always a member) and a person who reaches the tax ID only through granted access cannot: the web app asks for an account administrator. | The tax IDs you assign to it, each at the level that your role gives you. |
POST /v1/accounts/{account}/api-keys | Any key of your account that carries account:write. | Tax IDs inside the scope of the key that issues it, and never more. |
- The level comes from the person. An
ownerand anadminreach every tax ID of the account atoperate. Amemberreaches only the tax IDs on which it has a grant, at the level of that grant (roles). A key takes the level of the person who issued it on each tax ID, and it only goes down. - A key issued through the API answers to two limits. Its scope cannot exceed the scope of the key that issues it, and its levels cannot exceed what the person behind that key holds today.
accounts:readandaccounts:writeneed more. They are privileged scopes: the issuer must be an owner or an admin, and the account must hold a privilege that only Factuarea grants.
List your keys
GET /v1/accounts/{account}/api-keys returns your keys with cursor
pagination. Each key exposes its prefix, scopes,
tier, environment and lifecycle timestamps — never the secret. Revoked keys
stay in the list, so you can see the history.
curl https://api.factuarea.com/v1/accounts/{account}/api-keys \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"{
"data": [
{
"id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f",
"object": "api_key",
"name": "Production sync",
"prefix": "fact_live_8KqW3pXn",
"scopes": ["invoices:read", "invoices:write"],
"tier": "scale",
"environment": "live",
"created_at": "2026-06-01T09:30:00Z",
"last_used_at": "2026-06-23T18:04:11Z",
"expires_at": null,
"revoked_at": null,
"is_active": true,
"is_revoked": false
}
],
"has_more": false,
"next_cursor": null
}prefix is the first chars of the key — safe to log, it does not
authenticate. Use it to recognize a key in your own dashboards without ever
storing the secret.
The list, the retrieval, the rotation, the revocation and the change of scope
work on the keys whose whole scope fits inside the scope of the key you
authenticate with. A key that reaches a tax ID yours does not is not in your
list, and asking for it answers 404 api_key_not_found.
Create a key
POST /v1/accounts/{account}/api-keys mints a new key and returns its plaintext
secret exactly once. Store it the moment you receive it — there is no
endpoint to read it back later.
curl -X POST https://api.factuarea.com/v1/accounts/{account}/api-keys \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Reporting export",
"scopes": ["invoices:read", "pdfs:read"],
"scoped_company_ids": [
"0193f1a0-5c2e-7b41-8d06-9a1e2f3c4d5e",
"01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01"
]
}'Response (201, with a Location header that points to the new key):
{
"data": {
"id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162",
"object": "api_key",
"name": "Reporting export",
"prefix": "fact_live_1N0Fnyhh",
"scopes": ["invoices:read", "pdfs:read"],
"tier": "scale",
"environment": "live",
"created_at": "2026-06-24T08:15:00Z",
"last_used_at": null,
"expires_at": null,
"revoked_at": null,
"is_active": true,
"is_revoked": false,
"secret": "fact_live_1N0FnyhhR2VbY7TcA9eFmN5z"
}
}The secret field appears only in this 201 response (and after a
rotation). It is never returned by the list, retrieve or any other
endpoint. If you lose it you must rotate the key. Persist it to a secret
manager immediately — never to a log or a repository.
Send an Idempotency-Key so that a retry after a network failure does not
create a second key. The replay answers the same 201 with secret set to
null, because we never keep the secret: if you did not receive the first
response, revoke that key and create a new one.
Request body
| Field | Required | Notes |
|---|---|---|
name | yes | Human-readable label (1–120 chars). |
scopes | yes | One or more scopes from the closed catalog. At least one. See the scopes you can assign. |
environment | no | live (default) or test. See below. |
scoped_company_ids | no | The tax IDs the key reaches, by id (UUID v7). Omit it and the key reaches the same tax IDs as the key that creates it, and never more. Not accepted with environment: test. See the tax IDs a key reaches. |
expires_at | no | Future ISO 8601 instant after which the key stops authenticating. |
ip_allowlist | no | Optional list of allowed IPs / CIDR ranges (IPv4, IPv6, /N). A request from another address answers 401 ip_not_allowed. |
The tier is derived from the plan of your account — it is not settable
from the body and there is no separate capacity purchase. If you send tier, it
is ignored.
Two caps apply when you create a key, both by tier and both counted over
active keys. max_api_keys_exceeded (422)
says the company that holds the key already has the keys its plan allows.
portfolio_max_api_keys_exceeded
(422) applies once your account has two or more real tax IDs and is counted
over every active key of the account, not per tax ID, so adding a tax ID does
not raise it. Revoking a key frees its place. Rate limits
describes the limits of a portfolio.
The environment field
Every key belongs to one of two environments, fixed at creation and visible on the key object:
environment | Prefix | Operates on |
|---|---|---|
live | fact_live_ | Your real tax IDs, with real side effects (VeriFactu → AEAT, emails, webhooks). |
test | fact_test_ | The sandbox company of your account, isolated and with external effects switched off. |
Pass environment: test when creating a key to mint a sandbox credential;
omit it for a live key. The prefix mirrors the environment, so you can tell
them apart without decoding the key. See Test mode &
sandbox for what is switched off in test.
curl -X POST https://api.factuarea.com/v1/accounts/{account}/api-keys \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
-H "Content-Type: application/json" \
-d '{"name": "Integration tests", "scopes": ["invoices:read", "invoices:write"], "environment": "test"}'A fact_test_ key belongs to your real account — its sandbox company lives
inside it — and that decides what it can do with keys:
- It reaches only its sandbox. Declaring
scoped_company_idstogether withenvironment: testfails validation, because a test key has no other tax ID to reach. A live key cannot carry the sandbox either: mixing them answers422 business_rule_violationwith the subcodenif_scope_environment_mismatch. - It reads the account like a live key. Listing and retrieving keys with a test key answers as it does with a live one.
- It never writes. Creating, rotating, revoking or changing the scope of a
key with a test key answers
403 live_credential_requiredand changes nothing. That includes asking for a live key, and also asking for another test key: mint test keys with a live key or in the web app.
Test managed accounts covers the accounts that a test key can write to.
The tax IDs a key reaches
The scope of a key is the list of tax IDs it reaches, and GET /v1/me
enumerates it in scope. It is fixed when you create the key, and you can
change it later.
- Without
scoped_company_idsthe key reaches the same tax IDs as the key that creates it, never more. The tax IDs on which the issuing person does not reach the level that your scopes need are left out. - With
scoped_company_idsthe key reaches exactly those tax IDs, and they have to fit the scope of the issuing key: naming one outside it answers422 business_rule_violationwith the subcodenif_scope_exceeds_issuerand lists the tax IDs that do not fit, with nothing created. The scope is never trimmed in silence. - Which tax IDs can be named. Those of your account, and those that your
account reaches through a relation in force: granted
access or a managed account.
Anything else, and any identifier that does not exist, answers the same
422 invalid_param_valuewithparamscoped_company_ids, so the answer cannot be used to census tax IDs.
PATCH /v1/accounts/{account}/api-keys/{api_key}/nif-scope
replaces the scope of a key you already deployed. scoped_company_ids is the
complete resulting list, not a delta, with at least one tax ID:
curl -X PATCH \
https://api.factuarea.com/v1/accounts/{account}/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/nif-scope \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
-H "Content-Type: application/json" \
-d '{"scoped_company_ids": ["0193f1a0-5c2e-7b41-8d06-9a1e2f3c4d5e", "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01"]}'{
"data": {
"id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f",
"object": "api_key_nif_scope",
"scope": [
{ "id": "0193f1a0-5c2e-7b41-8d06-9a1e2f3c4d5e", "tax_id": "B12345674", "name": "Distribuciones Ejemplo SL" },
{ "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "tax_id": "B87654323", "name": "Talleres García SL" }
]
}
}- Only inside your scope. You can change a key only when its current scope fits inside yours, and the new scope can never exceed yours.
- The scopes of the key are checked again. Every scope the key carries has
to be allowed by the plan of every tax ID in the new list. Otherwise the
answer is
422 scope_not_allowed_by_planand nothing changes. - Levels. A tax ID that stays keeps the level stored on the key, so changing the scope never raises it. A tax ID that enters takes the level of the person who issued the key.
- Same answers as creating. The same
422errors apply, and a revoked key answers422 api_key_already_revoked. A test key has a fixed scope, its sandbox: any other list answersnif_scope_environment_mismatch.
Scoped keys explains how to use a key that reaches only a part of the account.
The level ceiling per tax ID
Each tax ID in a key has a level, view or operate. It is fixed when the
tax ID enters the key, at issue or when the scope changes, and it only goes down.
It is the lowest of three: the level of the relation, when the tax ID belongs to
another account that you reach through granted access or a managed account; the
grant of the person who issues the key; and the role of that person.
- A write needs
operate. A key with any scope that is not a:readone,*included, needsoperateon every tax ID it names. A key that only carries:readscopes is enough withview. - A tax ID the issuing person only reads. Naming it together with a write
scope answers
422 business_rule_violationwith the subcodenif_access_level_exceeds_emitterand creates nothing. If you declare noscoped_company_ids, the tax IDs below the needed level are left out, and when none is left the answer is the same422. - It is measured again on every request. What counts is the lower of the
level stored on the key and the level that the issuing person holds at that
moment. With
view, a write answers403 company_read_only, and with no level left the same404 profile_not_foundas a tax ID outside the scope. - It is recalculated downward. Lowering or removing a grant lowers the keys of that person, and narrowing or ending an access between accounts lowers the keys of the receiving account on the next request. A key left with no tax ID is revoked. Nothing goes up on its own: to get a higher level, issue a new key.
{
"error": {
"type": "invalid_request_error",
"code": "business_rule_violation",
"subcode": "nif_access_level_exceeds_emitter",
"message": "No puedes dar a la credencial más acceso del que tienes. Sin acceso suficiente en: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01. Una credencial con permisos de escritura solo alcanza NIF en los que operas.",
"param": "scoped_company_ids",
"doc_url": "https://docs.factuarea.com/guides/errors#business_rule_violation",
"request_id": "req_01J8Y3M2Q40000000000000011"
}
}Granted access shows the ceiling of a key on a tax ID that another account lets you reach, and what happens to the keys when that access narrows or ends.
The scopes you can assign
scopes takes values from the closed catalog of
Authentication. A value outside it fails
validation with 422 invalid_param_value.
- The plan decides, tax ID by tax ID. A scope is granted only if the plan of
every tax ID in the scope allows it, and the excess is never trimmed in
silence: it answers
422 scope_not_allowed_by_plan. A module blocked in test mode answers422 scope_not_allowed_in_sandbox. Themembers:*,invitations:*andnif_transfers:*scopes need theuser_managementmodule. accounts:readandaccounts:writecannot be assigned by yourself. They are privileged scopes: a key carries them only while the account holds a privilege that only Factuarea grants, and only if the person who issues the key is an owner or an admin. Otherwise the creation answers403 privileged_scope_requiredand no key is created.*does not cover them. The super-scope covers every other scope. With the privilege you can list*andaccounts:*together in the same key.
Managed accounts explains what the privilege opens.
Rotate the secret
POST /v1/accounts/{account}/api-keys/{api_key}/rotate-secret generates a fresh
prefix + secret and returns the new secret in plaintext exactly
once. The previous secret keeps working for a 24-hour grace
window so you can roll out the new one with zero downtime. The environment of
the key does not change.
It is irreversible, so Idempotency-Key is required: without it the answer is
422 idempotency_key_required.
curl -X POST \
https://api.factuarea.com/v1/accounts/{account}/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/rotate-secret \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
-H "Idempotency-Key: $(uuidgen)"{
"data": {
"id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f",
"object": "api_key",
"name": "Production sync",
"prefix": "fact_live_Zq7mP4xV",
"scopes": ["invoices:read", "invoices:write"],
"tier": "scale",
"environment": "live",
"created_at": "2026-06-01T09:30:00Z",
"last_used_at": "2026-06-23T18:04:11Z",
"expires_at": null,
"revoked_at": null,
"is_active": true,
"is_revoked": false,
"secret": "fact_live_Zq7mP4xVnR2VbY7TcA9eFmN5z"
}
}During the 24-hour grace window both the new and the previous secret
authenticate; a request still using the previous one receives a 199
Warning header counting down the hours left. Once the window expires
the previous secret is rejected and purged. Roll the new secret out
within those 24 hours. Rotation is irreversible.
Revoke a key
POST /v1/accounts/{account}/api-keys/{api_key}/revoke invalidates a key permanently.
Subsequent requests authenticated with it fail with 401. An optional
reason (max 500 chars) is recorded in the audit log. Like the rotation, it is
irreversible and requires an Idempotency-Key.
curl -X POST \
https://api.factuarea.com/v1/accounts/{account}/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/revoke \
-H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason": "Rotated out of the deploy pipeline"}'Revocation is irreversible and is not restricted to other keys: you may revoke the key you are authenticating the request with, cutting off your own access. Make sure another valid key is in place first if you still need API access.
After revocation, requests with that key return 401 with the code
api_key_revoked:
{
"error": {
"type": "authentication_error",
"code": "api_key_revoked",
"message": "Esta API key ha sido revocada.",
"doc_url": "https://docs.factuarea.com/guides/errors#api_key_revoked",
"request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV"
}
}The code is specific because the secret still matches: whoever sends it already
holds the key. A secret that matches nothing answers the generic
invalid_api_key, and an expired key answers
api_key_expired.
Scopes and isolation
The six endpoints are gated by the account scopes:
account:read— list and retrieve keys.account:write— create, change the scope of, rotate and revoke keys.
All operations are scoped to your own account and, inside it, to the scope of
the key you authenticate with. The answers never reveal what exists, and none of
them is a 403:
- An
{account}that is not yours, does not exist or is not an account identifier answers404 account_not_found. - A key
idthat belongs to another account, does not exist or reaches a tax ID outside your scope answers404 api_key_not_found.
Managing keys still requires an existing key with the right scopes. Create your first key in the developer dashboard (app.factuarea.com/settings/developers/api-keys), as an owner or an admin of the account, then use these endpoints to provision the rest programmatically. See Authentication for the key format and headers.
Errors
| Code | HTTP | When |
|---|---|---|
insufficient_scope | 403 | The key carries neither the scope of the operation nor *. |
live_credential_required | 403 | A fact_test_ key tries to create, rotate, revoke or change the scope of a key. |
privileged_scope_required | 403 | You ask for accounts:read or accounts:write and the account does not hold the privilege, or the issuer is no longer an owner or an admin. |
account_not_found | 404 | {account} is not your account. |
api_key_not_found | 404 | The key does not exist, belongs to another account or reaches a tax ID outside your scope. |
invalid_param_value | 422 | A scope is outside the catalog, or scoped_company_ids names a tax ID that does not exist or that your account does not reach. |
business_rule_violation | 422 | Subcodes nif_scope_exceeds_issuer, nif_access_level_exceeds_emitter, nif_scope_environment_mismatch and issuer_company_outside_account. |
scope_not_allowed_by_plan | 422 | A requested scope is not allowed by the plan of some tax ID of the scope. |
scope_not_allowed_in_sandbox | 422 | A test key asks for a scope of a module blocked in test mode. |
max_api_keys_exceeded | 422 | The company that holds the key has reached the number of keys its plan allows. |
portfolio_max_api_keys_exceeded | 422 | The account has reached the active keys that a portfolio allows. |
api_key_already_revoked | 422 | You try to rotate, revoke or change the scope of a key that is already revoked. |
idempotency_key_required | 422 | Rotate or revoke without an Idempotency-Key. |
MCP tools
The same operations are MCP tools: list_api_keys, get_api_key,
create_api_key, rotate_api_key_secret, revoke_api_key and
update_api_key_nif_scope. The two reads can also be used through OAuth. The
four writes need an API key, because OAuth consent never grants account:write.
See MCP scopes.
Store permissions
Store management uses stores:read and stores:write; provider-specific scopes are woocommerce_store:read, woocommerce_store:write, shopify_store:read and shopify_store:write. Connection-test operations require the provider write scope. These scopes are assigned to API keys and are outside OAuth consent. See ecommerce stores for the operation matrix.
Related
keys that reach only a part of the account.
the key format, the scope catalog, privileged scopes and OAuth.
the roles of the team, the grants per tax ID and GET /v1/me.
the level of a key on a tax ID of another account and what happens when the access narrows.
what a fact_test_ key switches off.