Factuarea APIDevelopers

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.

OperationEndpointScope
List keysGET /v1/accounts/{account}/api-keysaccount:read
Create a keyPOST /v1/accounts/{account}/api-keysaccount:write
Retrieve a keyGET /v1/accounts/{account}/api-keys/{api_key}account:read
Change the tax IDs of a keyPATCH /v1/accounts/{account}/api-keys/{api_key}/nif-scopeaccount:write
Rotate the secretPOST /v1/accounts/{account}/api-keys/{api_key}/rotate-secretaccount:write
Revoke a keyPOST /v1/accounts/{account}/api-keys/{api_key}/revokeaccount: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:

WhereWho canWhat the new key gets
The web app, in Settings → Developers → API KeysAn 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-keysAny 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 owner and an admin reach every tax ID of the account at operate. A member reaches 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:read and accounts:write need 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

FieldRequiredNotes
nameyesHuman-readable label (1–120 chars).
scopesyesOne or more scopes from the closed catalog. At least one. See the scopes you can assign.
environmentnolive (default) or test. See below.
scoped_company_idsnoThe 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_atnoFuture ISO 8601 instant after which the key stops authenticating.
ip_allowlistnoOptional 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:

environmentPrefixOperates on
livefact_live_Your real tax IDs, with real side effects (VeriFactu → AEAT, emails, webhooks).
testfact_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_ids together with environment: test fails validation, because a test key has no other tax ID to reach. A live key cannot carry the sandbox either: mixing them answers 422 business_rule_violation with the subcode nif_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_required and 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_ids the 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_ids the key reaches exactly those tax IDs, and they have to fit the scope of the issuing key: naming one outside it answers 422 business_rule_violation with the subcode nif_scope_exceeds_issuer and 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_value with param scoped_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_plan and 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 422 errors apply, and a revoked key answers 422 api_key_already_revoked. A test key has a fixed scope, its sandbox: any other list answers nif_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 :read one, * included, needs operate on every tax ID it names. A key that only carries :read scopes is enough with view.
  • A tax ID the issuing person only reads. Naming it together with a write scope answers 422 business_rule_violation with the subcode nif_access_level_exceeds_emitter and creates nothing. If you declare no scoped_company_ids, the tax IDs below the needed level are left out, and when none is left the answer is the same 422.
  • 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 answers 403 company_read_only, and with no level left the same 404 profile_not_found as 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 answers 422 scope_not_allowed_in_sandbox. The members:*, invitations:* and nif_transfers:* scopes need the user_management module.
  • accounts:read and accounts:write cannot 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 answers 403 privileged_scope_required and no key is created.
  • * does not cover them. The super-scope covers every other scope. With the privilege you can list * and accounts:* 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 answers 404 account_not_found.
  • A key id that belongs to another account, does not exist or reaches a tax ID outside your scope answers 404 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

CodeHTTPWhen
insufficient_scope403The key carries neither the scope of the operation nor *.
live_credential_required403A fact_test_ key tries to create, rotate, revoke or change the scope of a key.
privileged_scope_required403You 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_found404{account} is not your account.
api_key_not_found404The key does not exist, belongs to another account or reaches a tax ID outside your scope.
invalid_param_value422A 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_violation422Subcodes nif_scope_exceeds_issuer, nif_access_level_exceeds_emitter, nif_scope_environment_mismatch and issuer_company_outside_account.
scope_not_allowed_by_plan422A requested scope is not allowed by the plan of some tax ID of the scope.
scope_not_allowed_in_sandbox422A test key asks for a scope of a module blocked in test mode.
max_api_keys_exceeded422The company that holds the key has reached the number of keys its plan allows.
portfolio_max_api_keys_exceeded422The account has reached the active keys that a portfolio allows.
api_key_already_revoked422You try to rotate, revoke or change the scope of a key that is already revoked.
idempotency_key_required422Rotate 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.

On this page

Need a hand?Contact support