Factuarea APIDevelopers

Reach a tax ID through the company segment

Work on any tax ID your credential reaches by naming it in the company segment of the path — your own, granted access and managed accounts — with how to resolve its id, the answers you get and the retired X-Active-Profile header.

Every company resource of the v1 API hangs off /v1/companies/{company}/…, where {company} is the public id (UUID v7) of the tax ID the request operates on. It works the same on every company resource, invoices, contacts, series and the rest:

curl https://api.factuarea.com/v1/companies/$COMPANY_ID/invoices \
  -H "Authorization: Bearer $FACTUAREA_KEY"

One credential reaches all the tax IDs in its scope, and you pick the target per request, in the path, without re-authenticating or juggling a key per tax ID. This page covers that path segment. To hand an integration only a part of the account, scope the key instead.

When the segment names a tax ID your credential reaches, the whole request runs against its data: every read is filtered to it and every write lands on it. The tax ID is resolved before rate limiting, idempotency and usage accounting, so all three are attributed to the tax ID you name and not to the titular company of the credential.

The segment is not optional. There is no route to a company resource without it, so no request can end up on a tax ID you did not name, which is exactly the guarantee the retired header could not give.

The segment never widens your credential. Its scopes, tier and environment carry over untouched: a key holding only invoices:read that names a tax ID still cannot POST /v1/companies/{company}/invoices there (403 insufficient_scope), and a fact_test_ key stays in the sandbox whichever tax ID it names. Choosing a tax ID changes which one you act on, never what you are allowed to do.

What the scope of a credential can hold

The scope of a key is the list of tax IDs it reaches, fixed when the key is issued and changed with PATCH …/nif-scope. It can hold three kinds of tax ID, and each one carries a level, view or operate:

Tax IDHow it gets into the scopeLevel it carries
Your ownYou name it in scoped_company_ids when you issue the key, or the key inherits the scope of the key that issued it.The level of the person who issued the key on that tax ID.
Granted by another accountThe granted access is in force, and you name the tax ID when you issue the key or change its scope.The lowest of the level of the access and the level of the person who issued the key, which for a member is also capped by its grant.
Of a managed account you provisionedThe management is in force with a level other than none, and you name the tax ID the same way. See managed accounts.The lowest of your level on the managed account and the level of the person who issued the key.

A level is fixed when the tax ID enters the key and it only goes down: scoped keys explains how. When an access narrows or ends, the tax ID leaves the scope of your keys on the next request. The keys keep working on the rest, and one left with an empty scope is revoked.

The MCP server reaches the same tax IDs: it names the tax ID with the X-Company-Id header instead of a path segment.

Resolve the tax ID identifier

The segment takes the opaque id, never the tax_id. Three places hand it to you, and none of them requires you to build it by hand:

  • GET /v1/me introspects the calling credential and enumerates, under scope, every tax ID it reaches with its id, tax_id and name. It does so always, also when there is exactly one. Its account.id is the identifier that the /v1/accounts/{account}/… segment expects.
  • GET /v1/accounts/{account}/companies?tax_id=B12345674 looks up a tax ID of your own account by its fiscal identifier. A tax number outside your scope returns an empty list, never a rejection. It does not list tax IDs that you reach through a relation.
  • The tax IDs of a managed account come in companies[] of GET /v1/accounts/{account}, as managed accounts describes.
curl https://api.factuarea.com/v1/me \
  -H "Authorization: Bearer $FACTUAREA_KEY"
{
  "data": {
    "object": "me",
    "account": {
      "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a00",
      "name": "Asesoría Martínez",
      "environment": "live",
      "is_provider": true,
      "privileged_scopes": [
        "accounts:read",
        "accounts:write"
      ]
    },
    "api_key": {
      "id": "019e81ef-e1d4-7208-a51d-94c2c2a54a84",
      "name": "Producción",
      "prefix": "fact_live_1OSf9KdP",
      "scopes": [
        "account:read",
        "series:read",
        "series:write"
      ],
      "tier": "scale",
      "environment": "live",
      "created_at": "2026-01-15T09:30:00Z",
      "last_used_at": "2026-05-25T18:00:00Z",
      "expires_at": null
    },
    "scope": [
      {
        "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
        "tax_id": "B12345674",
        "name": "Acme Corporation"
      },
      {
        "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02",
        "tax_id": "B87654323",
        "name": "Acme Logística S.L."
      }
    ],
    "client_accounts": [
      {
        "id": "0199a1f0-1c22-7a10-b111-5a5b5c5d5e5f",
        "name": "Construcciones Ruiz",
        "origin": "granted",
        "access_level": "view",
        "status": "active",
        "paid_by_provider": false,
        "environment": "live"
      },
      {
        "id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
        "name": "Talleres García",
        "origin": "provisioned",
        "access_level": "operate",
        "status": "provisioned",
        "paid_by_provider": true,
        "environment": "live"
      }
    ],
    "client_accounts_visible": true,
    "client_accounts_has_more": false
  }
}

The id of each entry in scope is what goes in {company}. client_accounts lists the accounts of your clients that your account reaches, and only when the key carries the privileged accounts:read scope in effect: accounts and tax IDs describes every field.

Cache the mapping tax_id → id in your integration and refresh it when you register a tax ID: the id is stable for the life of the tax ID, and the tax_id is immutable after registration. An archived tax ID that is registered again is a new one, with a new id.

Resolution and errors

The company segment resolves the effective tax ID before any handler runs:

Company segmentOutcome
A tax ID in the scope of your credential, active, on which your level allows the requestThe request operates on that tax ID.
In scope, but deactivated (inactive) or archived403 company_inactive. A deactivated tax ID can be reactivated first; an archived one cannot, because archived is terminal.
Outside your scope, non-existent, not an identifier, or belonging to the other environment (live ↔ test)404 profile_not_found.
In scope, but your key holds view on it and the request writes403 company_read_only, with nothing created or changed.
In scope, but the person who issued the key no longer reaches it and the request writes404 profile_not_found, the same as outside the scope.
Reached through a relation between accounts, on an operation that belongs to the account that owns the tax ID403 forbidden_action, at any level and reads included.
An operation that your scopes do not cover403 insufficient_scope.

The 404 is indistinguishable in all its cases: the API never reveals that a tax ID outside your scope exists, and a live tax ID and its sandbox twin share a fiscal identifier. The environment is checked before the state, so a deactivated tax ID of the other environment also answers 404.

{
  "error": {
    "type": "not_found_error",
    "code": "profile_not_found",
    "message": "La empresa indicada no existe o no está en el ámbito de tu credencial.",
    "param": "company"
  }
}

The 403 company_inactive is different: the tax ID is within your scope, so revealing that it is deactivated is legitimate, and, for a deactivated tax ID, it is the signal to reactivate it before operating:

{
  "error": {
    "type": "authorization_error",
    "code": "company_inactive",
    "message": "Esta empresa está desactivada. Actívala para operar.",
    "param": "company"
  }
}

An archived tax ID answers the same 403 on this axis and cannot be reactivated. Through the account, the operations that change it answer 404.

A 403 company_read_only says that your access to the tax ID is read-only, and its reads keep working. It depends on the lower of the level stored in the key and the level that the person who issued the key holds today.

A 403 forbidden_action on a tax ID reached through a relation says that the operation belongs to the account that owns it. Those operations are the request logs, the events, the company webhook endpoints, billing, the automation usage and the writes of the payment credentials. Granted access lists them. The message does not name the owning account:

{
  "error": {
    "type": "authorization_error",
    "code": "forbidden_action",
    "message": "Esta operación solo la puede hacer la cuenta dueña del NIF: con un acceso por relación entre cuentas no está disponible.",
    "doc_url": "https://docs.factuarea.com/guides/errors#forbidden_action",
    "request_id": "req_01J8Y3M2Q40000000000000008"
  }
}

The account axis is another path

/v1/accounts/{account}/… names an account, not a tax ID. {account} must be the account of your credential: an account that is not yours, one that does not exist, a value that is not an identifier and one that granted you access all answer the same 404 account_not_found. A managed account is accepted only on a closed list of routes that managed accounts describes. The routes of the tax ID portfolio carry {company} as well, but there it names the tax ID the operation acts on, not the one that owns the data.

The retired X-Active-Profile header

Until this version, the target tax ID travelled in an X-Active-Profile request header. That header is retired. A request that carries it is rejected with 400, whatever its value:

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_unknown",
    "subcode": "retired_active_profile_header",
    "message": "La cabecera 'X-Active-Profile' fue retirada de la API v1: la empresa efectiva se indica en el segmento de empresa de la ruta.",
    "param": "X-Active-Profile"
  }
}

The rejection fires on presence, never on content: a well-formed identifier, a tax ID outside your scope, a malformed value, an empty string and a whitespace-only string all receive exactly the same error with the same message. Nothing about the value is interpreted, so the rejection cannot be used to probe which tax IDs exist.

Sending the header and being ignored would have been worse than being rejected: a write meant for another tax ID would have returned 201 with the document issued under the credential holder's NIF. The 400 is raised before the handler, so a rejected write leaves nothing behind. Migrating is mechanical: delete the header and move its value into the {company} segment of the path.

Which one should you use?

The path segment and the ways of widening or narrowing what a credential reaches solve different needs, and they coexist:

  • Your own tax IDs. Use one key of your account and name the tax ID per request. To give an integration only a part of the account, scope the key: its credential reaches only those tax IDs, and the segment can name no other.
  • The tax IDs of a business that keeps its own account. Granted access lets that business give your account view or operate over some or all of its tax IDs. You add them to a key and name them in the segment like your own.
  • Clients you onboard yourself. Managed accounts let you provision the account of a client over the API, and you reach its tax IDs at your level for as long as the management lasts.

Naming a tax ID in the path only switches which one you act on. It never changes the scopes of the credential, and isolation works as it does for managing the portfolio: a tax ID outside your scope is never observable, and the answer is a 404 and not a 403.

On this page

Need a hand?Contact support