Factuarea APIDevelopers

Granted access between accounts

Let another account reach some or all of your tax IDs with view or operate level, and what that account can and cannot do.

An account can let another account work with some or all of its tax IDs, without sharing a password and without moving anything. The account that lets it is the granting account, and the tax IDs stay its own. The account that receives the access is the receiving account. It is what a business uses when it wants its accounting firm to issue and read its documents.

It is one access between two accounts. It has a level, view or operate, and a scope: every tax ID of the granting account, or only the ones it picks. The receiving account reaches those tax IDs with its own credentials through the {company} segment of the company axis. This guide covers how an access is granted, what the receiving account can and cannot do with it, and what happens to its keys when the access narrows or ends.

It is not a managed account, which a provider creates for its client, and it is not a grant, which gives a member of your own account access to one of your tax IDs. When the owner of a managed account takes over the payment, the provider's access turns into a granted access: pricing and payer change explains it.

The access in short

AspectHow it works
Who grantsAn owner or admin of the granting account, from the web app.
Who receivesAn owner or admin of the receiving account, who accepts it from the web app.
Levelview reads. operate reads and writes. none is not a level you can grant.
Scopeall: every tax ID of the granting account, also the ones it adds later. list: only the tax IDs picked.
Open accessesAt most one pending or active access between the same two accounts. An ended one does not block a new one.
CostIt needs no plan and no module, and it takes no user seat from either account. The granting account keeps paying for its tax IDs.
EndingThe granting account can revoke it at any time and the receiving account can give it up at any time.

With all, a tax ID that the granting account adds later enters the access on its own, and one that it archives or transfers to another account leaves it at once. With list, the access covers the listed tax IDs that still belong to the granting account. A tax ID that is only deactivated stays in the scope and answers 403 company_inactive until it is reactivated.

Grant, change and revoke in the web app

Granting, changing and revoking an access is done only in the web app. There is no API route and no MCP tool for it, on purpose: a leaked credential must not be able to open the tax IDs of an account to another account. Accepting, declining and giving up an access are web app operations too.

AccountWhereWhat it does there
GrantingSettings → Access to my accountInvites, resends or withdraws an invitation, changes the level or the tax IDs, and revokes the access.
ReceivingSettings → Received accessAccepts or declines an invitation, gives up an active access, and shares it with its team.
  • Who can. Only an owner or admin of the account on each side. A member and a person outside the account get the same 403 forbidden_action.
  • Invite by email or by account code. By email, Factuarea sends the invitation to that address, and a person with no account yet can register and accept with the same link. By account code, the granting account pastes the code of the other account, which is its account.id, the UUID that GET /v1/me returns. The receiving account finds it in Settings → Access to my account, and the invitation then waits in its pending invitations. Each invitation carries a level and a scope.
  • One open access per pair. Inviting an account that already has a pending or active access with you answers 422 relation_already_exists, and so does accepting an invitation by email when the two accounts already have an open one. A code that names your own account, does not exist or is malformed answers the same 422 relation_self_grant_not_allowed, so the field cannot be used to probe accounts. A list of tax IDs that names one the granting account does not own, or that is empty, answers 422 relation_company_not_in_grantor and does not say which one fails.
  • Expiry. An invitation expires after 7 days. Resending it issues a new secret and a new expiry, and the previous link stops working. An expired invitation stays pending and cannot be accepted (422 relation_invitation_expired). One sent by account code keeps the pair occupied until it is resent or withdrawn. Inviting and resending share one rate limit for each granting account.
  • Accepting. An invitation by account code is accepted from the list of pending invitations, and one by link is accepted by choosing which account the person administers. An invitation by email must be accepted by the person with that address. The secret works once: one that is empty, unknown, already used, withdrawn or addressed to someone else answers the same 422 relation_invitation_invalid, on purpose.
  • Changing and ending. The granting account can change the level between view and operate, change the tax IDs and revoke the access at any time, also while the invitation is pending. The receiving account can decline a pending invitation and give up an active one. A change that the state of the access does not admit answers 422 relation_transition_not_allowed.
  • Who is told. An invitation, its acceptance, a revocation, a decline and a renunciation notify the other account by email. Changing the level or the tax IDs sends no email, and it reaches the receiving account on its next request.

Reach a granted tax ID

The receiving account reaches a granted tax ID like one of its own: through the {company} segment, in /v1/companies/{company}/…. {company} is the id of the tax ID (UUID v7), never its tax_id. The {account} segment of /v1/accounts/{account}/… stays your own account: the granting account is not an account you administer, and naming it answers the same 404 account_not_found as any account that is not yours. The MCP server reaches the same tax IDs: it names the tax ID with the X-Company-Id header instead of a path segment, with the same levels and the same limits.

A person of the receiving account reaches a granted tax ID with this level:

Role in the receiving accountLevel on a granted tax ID
owner (titular) and admin (administrator)The level of the access, with no grant needed.
memberThe lower of the level of the access and its own grant on that tax ID. With no grant, it does not reach the tax ID.

In the web app the granted tax IDs appear as client accounts, apart from the tax IDs of your own account. A credential gets the level of the person who issued it, as the ceiling of your keys explains.

A credential reaches a granted tax ID only when its scope names it. Keys that already existed do not change when an access is granted, and a key that you create through the API without scoped_company_ids reaches the same tax IDs as the credential that creates it, and never more. So:

  • The first key is created in the web app. In Settings → Developers → API Keys, the tax ID scope field lists the tax IDs of the access in a group called Client accounts, each with the highest level it accepts. Only an owner or an admin of the receiving account can create keys there and so add them: a member cannot, not even over the tax IDs on which it has a grant.
  • The API narrows and never widens. POST /v1/accounts/{account}/api-keys with scoped_company_ids and PATCH /v1/accounts/{account}/api-keys/{api_key}/nif-scope, both with the account:write scope, accept only tax IDs that the calling key already reaches. Asking for one more answers 422 business_rule_violation with the subcode nif_scope_exceeds_issuer, and a tax ID of an account that granted you nothing answers 422 invalid_param_value, the same as one that does not exist.

Once a key reaches them, GET /v1/me lists them in scope. They do not appear in GET /v1/accounts/{account}/companies, which lists only the tax IDs of the account in the path. The client_accounts block of GET /v1/me lists the accounts that granted you access (origin granted) only when the key carries the privileged accounts:read scope in effect, which needs the provider privilege of your account. Otherwise it is empty and client_accounts_visible is false.

What you can do at each level

The level of the access decides what you can do on a granted tax ID.

When you callWith viewWith operate
A read of the data of the tax ID (invoices, contacts, series, documents and so on)200200
A write of the data of the tax ID (create, change, send, delete)403 company_read_only, with nothing created or changedAllowed, as an administrator of the tax ID
One of the operations of the granting account on the company axis403 forbidden_action, reads included403 forbidden_action, reads included
A route of the granting account under /v1/accounts/{account}/…404 account_not_found404 account_not_found

An operate access makes you an administrator of the tax ID, not of the account that owns it. You work with the data of the tax ID within what the plan of the granting account allows, and what you create belongs to that tax ID. The MCP server applies the same levels and answers the same 403 company_read_only.

A POST /v1/companies/{company}/invoices on a tax ID with view answers:

{
  "error": {
    "type": "authorization_error",
    "code": "company_read_only",
    "message": "Tu acceso a esta empresa es de solo lectura.",
    "doc_url": "https://docs.factuarea.com/guides/errors#company_read_only",
    "request_id": "req_01J8Y3M2Q40000000000000007"
  }
}

What stays with the granting account

Some operations of the company axis belong to the account that owns the tax ID, not to the data of the tax ID. On a granted tax ID they answer 403 forbidden_action before anything is read or changed, at either level and reads included. The message does not name the granting account.

AreaOperationsWhy it stays with the owner
Request logsGET …/request-logs and GET …/request-logs/{request_id}They show the developer activity of the credentials of the owning account.
EventsGET …/events and GET …/events/{event}They are the stream of events that feeds the webhooks of the owning account.
Webhook endpointsThe eleven operations of …/webhook-endpoints: list, create, show, update, delete, deliveries, redelivery, ping, rotate-secret and test-eventThey set destinations, signing secrets and redeliveries that the owning account configures and pays.
BillingGET …/billingIt is the plan and the billing of the owning account, which only the account that pays decides and reads.
Automation usageGET …/automations/usageIt is the consumption of the automation quota of the plan of the owning account.
Payment credentialsPATCH …/stripe-autoinvoicing/config, PATCH …/connected-accounts/{account} and DELETE …/connected-accounts/{account}They configure or disconnect the payment credentials that the owning account connected. Reading them stays a read of the data of the tax ID.

A POST /v1/companies/{company}/webhook-endpoints on a granted tax ID answers, with view or operate:

{
  "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 MCP tools of these operations answer the same. Every other operation of the company axis is data of the tax ID and is served with your level. The account that owns the tax ID calls these same operations as usual.

The capabilities of the granting account itself live under /v1/accounts/{account}/…: its team, invitations, ownership, API keys, tax ID portfolio and transfers, account webhooks and request logs. {account} must be your own account, so you cannot address them. The operations that take a tax ID of the account in the path refuse a granted one too: activating, deactivating, editing or archiving a granted tax ID through your own account path answers 404 resource_not_found, with no change of state and no charge. The web app blocks the same capabilities while a granted tax ID is your active company: billing, team, invitations, ownership, keys, purchase, transfer and archive of tax IDs, company webhooks and the settings of the account stay with the granting account.

Your keys: level and ceiling

The level of a key on a granted tax ID is fixed when the tax ID enters the key, at issue or when the scope changes, and it only goes down. It is the lower of the level of the access and the level that the person who issues the key has on that tax ID, which for a member is also capped by its grant. On every write, the level that counts is the lower of the one stored on the key and the one that the issuing person holds today.

  • A write needs operate. A key with any scope that is not a :read one, the * scope included, needs operate on every tax ID it names. Naming a tax ID that only has view together with such a 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 level that the scopes need are left out, and when none is left the answer is the same 422. In the web app, a tax ID with view only accepts read scopes.
  • A tax ID the issuer does not reach. A granted tax ID on which a member has no grant, or one of an account that gave you no access, answers 422 invalid_param_value with param scoped_company_ids, the same as a tax ID that does not exist.
  • The plan is the plan of the tax ID. A granted tax ID brings the plan of its own account. A key gets a scope only if the plan of every tax ID in its scope allows it, and the excess is never trimmed in silence: it answers 422 scope_not_allowed_by_plan.
  • Test keys do not reach it. A fact_test_ key reaches only the sandbox company of its account, so it never reaches a granted tax ID and answers 404 profile_not_found. Use a fact_live_ key.

When the access narrows or ends

An access narrows when the granting account revokes it, lowers its level or removes tax IDs from it, when the receiving account gives it up, and when a tax ID leaves the granting account. Each of these takes effect on the next request:

  • The grants that members of the receiving account held on the lost tax IDs are removed.
  • Every key of the receiving account is recalculated downward. A tax ID with no level left leaves the scope of the key, a level lowered to view turns the writes of that key into 403 company_read_only, and a key left with an empty scope is revoked. The rest of the key keeps working on its own tax IDs.
  • A tax ID that the receiving account no longer reaches answers 404 profile_not_found, the same as a tax ID that does not exist, so the answer does not reveal that it does.

Nothing goes up on its own. Raising the level, widening the tax IDs or accepting a new access does not touch the keys that already exist: issue a new key for that tax ID to get the new level.

Share the access with your team

An owner or admin of the receiving account decides which members of its team work with the granted tax IDs. A member gets a grant with view or operate on a tax ID of the scope of an active access, and reaches it with the lower of the grant and the level of the access. The web app does it from the client account, under Received access. The API mirrors it with the operations of the member grants, where {company} is the granted tax ID:

OperationScopeWhat it does
GET /v1/accounts/{account}/members/{member}/grants/{company}members:readRetrieves the grant on one tax ID.
PUT /v1/accounts/{account}/members/{member}/grants/{company}members:writeCreates or replaces the grant on one tax ID.
DELETE /v1/accounts/{account}/members/{member}/grants/{company}members:writeRevokes the grant on one tax ID.
  • Who and what. The person who issued the credential must be an owner or admin of the account, and the scope of the credential must already include that tax ID. The members:* scopes need the user_management module.
  • The lower level wins. Giving operate over an access that is view is not rejected: the level is stored as given, and the member reaches the tax ID with the lower of the two.
  • Outside the access. A tax ID outside an active access, or one whose access ended, answers 422 grant_company_not_in_account when you write and 404 member_not_found when you read, without saying that an access ever existed. An owner or an admin answers 422 grants_only_for_member, because it reaches every tax ID of the access already.
  • No email. Giving, changing or removing a grant sends no email.

Follow the access as the granting account

The granting account can follow its accesses with three account.* events on its account webhooks:

EventWhen it is sent
account.access_grantedThe receiving account accepts and the access becomes active.
account.access_level_changedThe granting account changes the level. data.previous_level carries the previous one.
account.access_revokedAn active access ends: the granting account revokes it or the receiving account gives it up. Withdrawing or declining an invitation that was never accepted is not published.

Their data is an account_relation object with id, origin (granted), level, previous_level, grantor_account_id, grantee_account_id and status. Only the endpoints of the granting account receive them: the receiving account is sent nothing about a granted access. A change of the tax IDs of the access is not published either.

The receiving account cannot subscribe to what happens in a granted tax ID. The webhook endpoints of the company axis answer 403 forbidden_action on it, as the table above says, and its account webhooks cover only its own tax IDs and the accounts it provisions.

Keep in mind

  • Email identity. A granted tax ID inherits nothing from the receiving account: its trust state and its sender identity check stay those of its own account. Sending documents describes the check.
  • Automations. The portfolio automations of the titular company of the receiving account cover, besides its other tax IDs, the granted tax IDs on which the access is operate, and none with view. Automations covers them.

On this page

Need a hand?Contact support