Factuarea APIDevelopers

Managed accounts

Provision accounts for your clients over the API: create them, claim voucher, access level, end of management and the routes a managed account opens.

A managed account is the account you create for one of your clients. It is a normal account, separate from yours: it has its own tax ID, its own fiscal profile, its own plan and, once the client claims it, its own holder. You provision it over the API, your account pays for it and you keep an access level on it (none, view or operate) until the client takes over. This guide is written for the account that provisions, the provider. It covers the privilege you need, how to create and read managed accounts, how the client claims one, how your level and the end of the management work, which routes a managed account opens and how to upload the client's VeriFactu certificate so its tax ID can issue.

What you pay for each managed account is in managed account pricing and payer change. The whole flow with a fact_test_ key, with no charges, is in test managed accounts. To create many at once, use the bulk import. The account you operate yourself, and the {account} segment, are in accounts and tax IDs.

The life of a managed account

Two fields say where a managed account is. status is the state of the account itself, and the management block says whether your account still manages it.

statusWhat it means
provisionedYou created it and nobody has claimed it yet, so it has no holder. Your account pays for it.
claimedThe client claimed it with the voucher and is now its owner. You keep your level and, until the client takes over the payment, you keep paying.
activeThe client runs it and pays for it: they took over the payment, or the management ended after the claim.
management.statusWhat it means
activeYour account manages it.
endedThe management ended and your account lost access. It is read in management, never in status.

An account whose management ends before anyone claims it stays provisioned.

Privileged scopes

The provisioning operations of this guide need accounts:read or accounts:write. They are privileged scopes, and they differ from every other scope in four ways:

  • * does not cover them. A key that only holds * gets 403 insufficient_scope on these operations. List accounts:read and accounts:write explicitly, next to * if you also want the rest.
  • We grant them, account by account. Factuarea grants the account provider privilege to an account, and only Factuarea can. Ask us for it: the API has no operation that grants it. GET /v1/me tells you whether you have it, with account.is_provider and account.privileged_scopes. A key can only carry accounts:* while that list names them, so creating such a key without the privilege answers 403 privileged_scope_required.
  • They do not come through OAuth. Use an API key, also with the MCP server.
  • No plan gates them. The gate is the privilege, not the plan.

The check runs on every request and nothing comes from a cache. The key must list the scope, your account must hold the privilege today and the person who issued the key must still be an owner or admin of your account. If any of the three fails, the answer is 403 privileged_scope_required, and both causes answer alike. If we revoke the privilege, or that person stops being an owner or admin, the same key stops working on its next request.

The plan and the import operations have their own guides: managed account pricing and payer change and bulk import.

The environment of the key is the environment of everything it provisions and reads. A fact_live_ key works with live managed accounts and a fact_test_ key with test ones, and each sees only its own.

{
  "data": {
    "account": {
      "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a00",
      "name": "Asesoría Martínez",
      "environment": "live",
      "is_provider": true,
      "privileged_scopes": [
        "accounts:read",
        "accounts:write"
      ]
    }
  }
}

The account block of GET /v1/me, shortened. The same response lists the managed accounts your account reaches in client_accounts, with origin: "provisioned", up to 100 of them; GET /v1/accounts lists all.

Provision an account

POST /v1/accounts creates the account, its tax ID and the relation with your account in one call. It has no {account} segment, because the account does not exist yet. The provider is always the account of the key and the environment is the one of the key: neither goes in the body.

FieldRequiredNotes
external_refYesYour own reference for the account, up to 128 characters, unique for your account. It makes the call idempotent.
planYesemprendedor, empresario or integral. enterprise is only assigned by us, by contract: 422 managed_account_plan_not_self_serve.
tax_idYesThe client's NIF, CIF or NIE, up to 20 characters, validated with its control character.
legal_nameYesLegal name, 2 to 100 characters. Without display_name, it is also the name of the account.
access_levelNoThe level your account keeps on it: none (the default), view or operate.
display_nameNoVisible name of the account and of its tax ID, 2 to 120 characters.
address, city, postal_code, province, countryNoThe fiscal address of the client. The postal code derives the AEAT zone.
languageNoes (the default), en or ca. The language of the account and of its voucher.
owner_emailDependsThe email of the person who will hold the account. Required with a live key, and whenever send_email is true. With a test key it is optional when no email is sent.
send_emailNofalse by default. It decides who delivers the voucher.
curl -X POST https://api.factuarea.com/v1/accounts \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "external_ref": "cliente-0042",
    "plan": "empresario",
    "access_level": "operate",
    "tax_id": "B12345674",
    "legal_name": "Talleres García SL",
    "display_name": "Talleres García",
    "language": "es",
    "owner_email": "laura@talleresgarcia.example",
    "send_email": false
  }'
{
  "data": {
    "id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
    "object": "managed_account",
    "external_ref": "cliente-0042",
    "status": "provisioned",
    "environment": "live",
    "plan": "empresario",
    "pending_plan": null,
    "billing_interval": "monthly",
    "access_level": "operate",
    "display_name": "Talleres García",
    "language": "es",
    "companies": [
      {
        "id": "0199a1f0-1c22-7a10-b111-7a7b7c7d7e7f",
        "tax_id": "B12345674",
        "name": "Talleres García SL"
      }
    ],
    "claimed": false,
    "claimed_at": null,
    "management": {
      "status": "active",
      "ended_at": null
    },
    "paid_by_provider": true,
    "created_at": "2026-09-27T09:00:00+00:00",
    "claim_token": "c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1",
    "claim_url": "https://app.factuarea.com/account-claims/accept?token=c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1c1a1",
    "claim_expires_at": "2026-10-11T09:00:00+00:00"
  }
}

A new account answers 201 Created with Location: /v1/accounts/{id}. The body is the managed account plus its claim voucher. companies[].id is the {company} you use on the tax ID axis, and id is the {account} of the routes that name a managed account.

Your account pays for it. In live mode we charge it on the managed-accounts subscription of your account, and managed account pricing and payer change explains how much and when. A fact_test_ key creates a test account instead: nothing is charged, there is no voucher (claim_token and claim_url are null) and it does not count against your cap.

Idempotency-Key is required, because provisioning cannot be taken back. A request without it is rejected with 422 idempotency_key_required before any business logic runs. A rejection creates nothing.

CodeStatusWhen
insufficient_scope403The key does not list accounts:write. * does not count.
privileged_scope_required403Your account does not hold the privilege, or the person who issued the key is no longer an owner or admin of it.
invalid_param_value422A field fails validation. error.param names it.
managed_account_plan_not_self_serve422plan is enterprise.
managed_account_provider_in_trial422Your account is in its trial and asked for a live account, which is charged. During the trial, only test accounts.
managed_accounts_limit_reached422Your account is at its cap of live managed accounts.
business_rule_violation with subcode nif_already_claimed_by_another_account422The tax_id of a live account already belongs to another account. A tax ID cannot be in two accounts at once. error.param is tax_id.
payment_method_required402A live account needs a payment method on your account, and there is none. Add one and repeat the call.
provisioning_account_claimed409The external_ref names an account its holder already claimed.
managed_account_external_ref_in_other_environment409The external_ref is used by an account of the other environment.
idempotency_key_required422The request carries no Idempotency-Key.
idempotency_key_reused422That key was already used with a different body.

Idempotency: Idempotency-Key and external_ref

Two protections work together, and they do different jobs.

Idempotency-Key protects one request. If the connection drops and you repeat the same request with the same key, we replay the first response, with the header Idempotent-Replayed: true, and provision nothing twice. The voucher is the one exception: we never store it in the clear, not even in the idempotency cache, so a replay answers claim_token and claim_url as null. If you never received the first response, reissue the voucher. The same key with another body is 422 idempotency_key_reused.

external_ref protects the account. It is unique for your account across both environments, so it is the natural key of your CRM. Provisioning an external_ref your account already provisioned, even with a new Idempotency-Key, never creates a second account:

The external_ref is already…What the call does
Under your management, in the same environment200 OK with that same account. Nothing is created, changed or charged, and no voucher is issued. The plan, the level and the tax ID you send are ignored.
An account of the other environment409 managed_account_external_ref_in_other_environment, with nothing written. Use another reference.
An account whose management you ended before anyone claimed it200 OK. The management is reactivated at the level you send and its tax IDs resume. For a live account, your account pays for it again and the same checks as for a new one run: trial, cap and payment method.
An account its holder already claimed409 provisioning_account_claimed, with nothing reactivated.

The claim voucher

The voucher is what the client needs to claim the account. It comes in three fields of the provisioning response:

FieldWhat it is
claim_tokenThe single-use secret. We keep only its fingerprint, never the voucher itself.
claim_urlThe link to the claim page in the app. It carries the token.
claim_expires_atWhen the voucher expires: 14 days after it was issued.

The voucher is returned once. claim_token and claim_url appear in the response that issues the voucher and never again: not in a replay, not in a list and not in a detail. Treat them as a credential and deliver or store them right away. If you lose one, reissue it: the previous one stops working.

What you get back depends on the call:

Callclaim_token and claim_url
New live account, send_email: false (the default)Returned, once, in the 201.
New live account, send_email: truenull. The link travels only in the email to owner_email.
New test accountnull. A test account has no voucher.
external_ref already provisioned (200)null. No voucher is issued.
Idempotent replaynull.
List and detailNever present.

The voucher is issued in the name of owner_email: a person who signs in with another address cannot use it.

Who delivers the voucher: send_email

You choose one of two ways, and the voucher never travels both ways.

  • send_email: false (the default). The response carries claim_url once and you deliver it through your own channel, for example your CRM or your own email. owner_email is still required with a live key, because the voucher is issued in its name.
  • send_email: true. We email the link to owner_email, in the language of the account, and the response carries null in claim_token and claim_url. owner_email is required.

Reissue the voucher

POST /v1/accounts/{account}/claim-tokens issues a new voucher for a managed account its holder has not claimed yet. {account} is the id of the managed account. The pending vouchers stop working and the new one goes to the same email and language. The body is optional and takes send_email, with the same meaning as above. It needs an Idempotency-Key.

curl -X POST https://api.factuarea.com/v1/accounts/$MANAGED_ACCOUNT_ID/claim-tokens \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "send_email": false }'
{
  "data": {
    "object": "managed_account_claim",
    "account_id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
    "claim_token": "d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2",
    "claim_url": "https://app.factuarea.com/account-claims/accept?token=d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2d2b2",
    "expires_at": "2026-10-11T09:00:00+00:00"
  }
}

With send_email: true, claim_token and claim_url are null, but expires_at still carries the date: the expiry is not a secret and a reissue always issues a new voucher. An idempotent replay returns claim_token and claim_url as null too, and keeps expires_at. The new voucher lasts 14 days, like the first one.

CodeStatusWhen
provisioning_account_claimed409The holder already claimed the account. There is nothing left to reissue.
managed_account_not_claimable422It is a test account, and a test account is never claimed.
idempotency_key_required422The request carries no Idempotency-Key.
account_not_found404{account} is not an account you manage. See which routes accept a managed account.

List and read your managed accounts

GET /v1/accounts lists the accounts your account provisioned, in the environment of the key only: a fact_test_ key lists your test accounts and a live key your live ones. No item carries a voucher.

ParameterNotes
statusprovisioned, claimed or active filter by the status of the account. ended returns the accounts whose management ended. Without status, the list holds only the managements in force.
external_refYour reference, an exact match: zero or one result. Up to 128 characters.
limitBetween 1 and 100, 25 by default.
starting_afterThe id of the last account of the previous page, which is also its next_cursor.
curl -G https://api.factuarea.com/v1/accounts \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  --data-urlencode "status=provisioned" \
  --data-urlencode "limit=25"
{
  "data": [
    {
      "id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
      "object": "managed_account",
      "external_ref": "cliente-0042",
      "status": "provisioned",
      "environment": "live",
      "plan": "empresario",
      "pending_plan": null,
      "billing_interval": "monthly",
      "access_level": "operate",
      "display_name": "Talleres García",
      "language": "es",
      "companies": [
        {
          "id": "0199a1f0-1c22-7a10-b111-7a7b7c7d7e7f",
          "tax_id": "B12345674",
          "name": "Talleres García SL"
        }
      ],
      "claimed": false,
      "claimed_at": null,
      "management": {
        "status": "active",
        "ended_at": null
      },
      "paid_by_provider": true,
      "created_at": "2026-09-27T09:00:00+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

An unknown status value or an external_ref longer than 128 characters answers 422, and an unknown query parameter or a malformed limit or starting_after answers 400.

GET /v1/accounts/{account} returns one managed account by its id, in the same shape. It works with any access level, none included: the provisioning operations only ask that the management is in force.

FieldMeaning
idThe {account} of the routes that name a managed account.
external_refYour own reference.
statusprovisioned, claimed or active, as above.
environmentlive or test: the environment of the key that provisioned it. A test account is never charged and cannot be claimed.
plan, pending_planThe current plan. pending_plan is a downgrade that takes effect at the end of the period, or null.
billing_intervalmonthly: managed accounts are billed monthly.
access_levelThe level your account keeps on it.
display_name, languageIts visible name, and the language of the account and of its voucher.
companiesIts tax IDs, each with id (the {company} of the tax ID axis), tax_id and name.
claimed, claimed_atWhether the holder claimed it, and when.
managementstatus (active or ended) and ended_at.
paid_by_providertrue while your account pays for it, false once the holder took over the payment.
created_atWhen it was provisioned.

With status=ended, your account has lost access to the account, so each item is reduced to what you already knew:

{
  "id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
  "object": "managed_account",
  "external_ref": "cliente-0042",
  "management": {
    "status": "ended",
    "ended_at": "2026-10-05T10:00:00+00:00"
  }
}

Provisioning usage

GET /v1/accounts/{account}/provisioning-usage tells you how many managed accounts you run and how many more you can provision. Unlike the routes above, {account} is your own account, the account.id of GET /v1/me: any other value answers 404 account_not_found.

{
  "data": {
    "object": "provisioning_usage",
    "managed_accounts": {
      "live": 12,
      "test": 3
    },
    "limit": 50,
    "remaining": 38,
    "managed_accounts_by_plan": {
      "emprendedor": 7,
      "empresario": 5,
      "integral": 0,
      "enterprise": 0
    },
    "provider_paid_additional_nifs": 2,
    "provider_paid_employee_seats": 9
  }
}
FieldMeaning
managed_accounts.live, managed_accounts.testThe managed accounts whose management is in force, by environment. Both counters always come back, whatever the environment of the key. Test accounts never count against the limit.
limit, remainingYour cap and the live accounts you can still provision. remaining is never negative.
managed_accounts_by_planThe live accounts by plan. It always lists every plan of the catalog, including those at 0.
provider_paid_additional_nifs, provider_paid_employee_seatsWhat your account pays for your managed accounts on top of their own fee: additional tax IDs and employee seats.

The claim

The client claims the account in the app, not through the API. The claim link opens a page where the client sees the account that is waiting, with its plan and its price. A new person chooses a password, and someone who already has a Factuarea account signs in. Either way they must use the address the voucher was issued to (owner_email): another address is rejected, and so is a voucher that was already used, revoked or has expired.

The claim has these effects:

  • The client becomes the owner of the account. claimed is true, claimed_at is set and status moves from provisioned to claimed. If you had already ended the management, the account goes straight to active.
  • You keep your level. The claim never changes access_level. After it, you can lower the level but no longer raise it: see the level rules.
  • You keep paying until the client takes over. While paid_by_provider is true, the account is yours to pay. Managed account pricing and payer change explains how the payment passes to the client.
  • We tell you. The owners and admins of your account get an email saying the account was claimed.
  • The voucher is consumed. Reissuing it answers 409 provisioning_account_claimed.

A test account cannot be claimed: 422 managed_account_not_claimable when you try to reissue its voucher.

Your access level

PATCH /v1/accounts/{account}/access-level changes the level your account keeps on a managed account. The body takes level, which is required.

curl -X PATCH https://api.factuarea.com/v1/accounts/$MANAGED_ACCOUNT_ID/access-level \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "level": "view" }'

The answer is 200 with the managed account and its new access_level.

LevelWhat your credentials reach
noneNothing of the client's data. You provision the account, you pay for it and you can still read it and use the provisioning operations, but no tax ID of it is reachable.
viewThe tax IDs of the account, read-only. A write on one of them answers 403 company_read_only.
operateThe tax IDs of the account, to read and write. It never gives you what only the client's own account can do: its company webhooks and request logs, for example, answer 403 forbidden_action.

Before the claim, you can raise and lower the level as often as you need. After the claim, you can lower it at any time, but raising it answers 422 access_level_raise_forbidden and changes nothing: only the holder can raise it, from their panel. Lowering takes effect at once, and narrows what your credentials reach in its tax IDs from their next request.

End the management

DELETE /v1/accounts/{account}/management ends the management of a managed account. From the next request your account loses access to it and to its tax IDs, and every route answers 404. It needs an Idempotency-Key, and the answer is 200 with the account and management.status: "ended".

curl -X DELETE https://api.factuarea.com/v1/accounts/$MANAGED_ACCOUNT_ID/management \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
{
  "data": {
    "id": "0199a1f0-1c22-7a10-b111-6a6b6c6d6e6f",
    "object": "managed_account",
    "external_ref": "cliente-0042",
    "status": "provisioned",
    "environment": "live",
    "plan": "empresario",
    "pending_plan": null,
    "billing_interval": "monthly",
    "access_level": "operate",
    "display_name": "Talleres García",
    "language": "es",
    "companies": [
      {
        "id": "0199a1f0-1c22-7a10-b111-7a7b7c7d7e7f",
        "tax_id": "B12345674",
        "name": "Talleres García SL"
      }
    ],
    "claimed": false,
    "claimed_at": null,
    "management": {
      "status": "ended",
      "ended_at": "2026-09-28T10:00:00+00:00"
    },
    "paid_by_provider": true,
    "created_at": "2026-09-27T09:00:00+00:00"
  }
}

What happens next depends on whether the client had claimed the account:

  • Before the claim. The account stays provisioned and is not operable: all its tax IDs become inactive, with suspension_reason: "management_ended". The voucher keeps working. The account stays that way until the client claims it, or until you provision the same external_ref again, which reactivates the management.
  • After the claim. The account becomes active and the payment passes to the holder. If the holder has no payment method, the management ends anyway: you stop paying and the tax IDs of the account stay suspended until the holder takes over the payment.
  • An enterprise account by contract that you really pay needs a plan to continue on. Without it, the answer is 422 enterprise_contract_transition_required and the management stays in force. Contact us so we set the plan, then repeat the call.

Afterwards, GET /v1/accounts?status=ended still lists the account with its id, your external_ref and the management block, so your CRM can close the loop. The client can also end the management from their panel.

Which {account} routes accept a managed account

{account} normally names your own account. A managed account is accepted on a closed list of routes, and only when your account provisioned it, its management is in force and it is in the same environment as the key.

RoutesManaged account
The provisioning routes: GET /v1/accounts/{account}, PATCH …/access-level, DELETE …/management, POST …/claim-tokens, PATCH …/plan and GET …/plan/previewAccepted, with any level, none included. The plan operations also ask for operate, as managed account pricing and payer change explains. These routes accept only a managed account: your own account answers 404 account_not_found here.
The tax ID data of the account: GET /v1/accounts/{account}/companies, POST …/companies, GET …/companies/stats, GET …/companies/seat-charge-preview, POST …/companies/activate, GET …/companies/{company}, PATCH …/companies/{company}, DELETE …/companies/{company}, POST …/companies/{company}/activate, POST …/companies/{company}/deactivate, POST …/companies/{company}/verify-creation, GET …/companies/{company}/creation-status and GET …/companies/{company}/issuing-readinessAccepted with a level other than none. Writes need operate: with view, a write on one tax ID answers 403 company_read_only, and registering or reactivating a tax ID, which is billed, answers 403 billing_managed_by_provider.
Everything else of the account axis: members, invitations, tax ID transfers, ownership, API keys, usage, employee seats, the workforce summary, provisioning usage, account webhooks and request logs404 account_not_found. These are capabilities of the account itself.

Anything a managed account is not accepted on answers the same 404 account_not_found, with the same body, in every case: an account that is not yours, one that does not exist, one in the other environment, one whose management ended, one that gave you granted access instead of being provisioned by you, and a value that is not an identifier. Nothing in the answer tells them apart, so it cannot be used to find out which accounts exist.

The managed account's tax IDs are also reachable on the tax ID axis, at /v1/companies/{company}/…, with the companies[].id you read above. Your keys reach them as they reach your own tax IDs, limited by your level and by the tax ID scope of each key. To add the client's tax IDs to a key you already deployed, send the complete resulting list to PATCH /v1/accounts/{account}/api-keys/{api_key}/nif-scope as scoped_company_ids. A new scope can never exceed the one of the credential that changes it.

Once the client takes over the payment, the account pays for its own tax IDs: your relation becomes granted access with the same level, and registering, activating, deactivating or archiving tax IDs is for the account that pays. Additional tax IDs explains what you can add while you pay.

The client's side is in the app

What the client does with a managed account happens in the app, and none of it has a /v1 operation or an MCP tool. A credential of yours cannot decide for the holder. The client uses the app to:

  • claim the account with the voucher;
  • see which account manages theirs and at which level;
  • raise or lower the level you keep, which is the only way to raise it after the claim;
  • end the management;
  • take over the payment.

Everything in this guide is your side, the provider's.

The cap: max_managed_accounts

Each account that provisions has a cap on its live managed accounts. It is 25 by default, it lives on your account as max_managed_accounts and only we change it. provisioning-usage returns it as limit.

  • What counts. Only live managed accounts whose management is in force. Test accounts never consume it, and neither do accounts whose management ended.
  • When it is reached. Provisioning one more live account, or reactivating one, answers 422 managed_accounts_limit_reached and creates nothing. Two requests at the same time cannot get past the cap together, because your account is locked while each one is checked.
  • How to make room. End the management of an account you no longer run, or ask us to raise the cap of your account.

The client's VeriFactu certificate

Each tax ID submits to VeriFactu with its own certificate. A provider does not sign on behalf of its clients, and there is no representation or power-of-attorney mode: what you can do with operate is upload the client's certificate, so its tax ID can issue.

The certificate must be the client's own FNMT certificate, in PKCS#12 format, and it must match the tax ID it is uploaded to. You need a key whose effective level on that tax ID is operate, with the verifactu:write scope, and the id of the tax ID from companies[].id of the managed account. With view the upload answers 403 company_read_only, and with none it answers the same 404 as any tax ID you cannot reach.

POST /v1/companies/{company}/verifactu/certificates takes multipart/form-data with certificate_file (.p12 or .pfx, up to 100 KB) and certificate_password. It needs an Idempotency-Key. The uploaded certificate is activated at once and the previous ones are deactivated.

curl -X POST https://api.factuarea.com/v1/companies/$CLIENT_COMPANY_ID/verifactu/certificates \
  -H "Authorization: Bearer $FACTUAREA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "certificate_file=@talleres-garcia.p12" \
  -F "certificate_password=$CERTIFICATE_PASSWORD"
{
  "data": {
    "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a13",
    "object": "verifactu_certificate",
    "subject_nif": "B12345674",
    "subject_name": "Talleres García SL",
    "subject_kind": "juridica",
    "valid_from": "2026-01-01T00:00:00+01:00",
    "valid_to": "2029-01-01T00:00:00+01:00",
    "is_active": true,
    "revoked_at": null,
    "created_at": "2026-01-10T09:00:00+01:00"
  }
}

The upload is rejected with 422 in these cases, and error.param names the field:

Code and subcodeparamWhen
verifactu_not_eligible with certificate_nif_mismatchcertificate_fileThe tax ID of the certificate is not the one of the company.
verifactu_not_eligible with certificate_expiredcertificate_fileThe certificate has expired or is not valid yet.
business_rule_violation with invalid_certificate_passwordcertificate_passwordThe password does not open the certificate.

What blocks a tax ID from issuing

GET /v1/accounts/{account}/companies/{company}/issuing-readiness answers whether a tax ID can issue right now, and why not when it cannot. It works with the {account} of a managed account and needs companies:read. can_issue is the verdict, and blockers is a closed list of reasons, in no particular order.

{
  "data": {
    "can_issue": false,
    "blockers": [
      "verifactu_certificate_missing"
    ],
    "census_status": "verified",
    "has_default_series": true,
    "fiscal_profile_complete": true,
    "operable": true,
    "missing_modules": [],
    "checked_at": "2026-09-30T10:15:00+00:00"
  }
}
BlockerWhat stands in the way
tax_id_missingThe company profile has no tax ID.
tax_id_invalid_formatThe tax ID is present, but it is not a valid NIF, CIF or NIE.
census_not_identifiedThe AEAT census check does not identify the holder of the tax ID.
census_revokedThe AEAT census check reports the tax ID as revoked.
census_unavailableThe census check is not available, so we cannot say the tax ID can issue.
fiscal_profile_incompleteFiscal data that issuing needs is missing: address, legal name or regime.
no_default_seriesThe tax ID has no default series for invoices.
company_not_operableThe tax ID is in your scope, but it is not operable, for example because it is inactive.
module_unavailableA module that issuing needs is not granted. missing_modules names it.
verifactu_certificate_missingVeriFactu is on for the tax ID and it has no valid certificate: none, revoked or expired.

verifactu_certificate_missing is a value of blockers, not an error code: the operation answers 200 and the catalog of error codes has no page for it. It is added to whatever else blocks the tax ID, so a tax ID can carry several at once. It only applies while VeriFactu is on for the tax ID. Upload the certificate and read issuing-readiness again: the blocker disappears when the certificate is valid.

The list can grow over time. Treat a reason you do not know as a blocker you cannot fix through the API. The read is neutral: it looks at the fiscal state and the modules, and never at the subscription, the plan or the payments.

A test account has no VeriFactu: the module is not available with a test key. Test managed accounts explains what the test path leaves out.

Follow a managed account with events

An account webhook endpoint of your account receives the events of the managed accounts you provision when they meet the delivery rules of that guide: account.managed_account_provisioned, account.managed_account_claimed, account.management_ended and account.access_level_changed. They are the way to learn, without polling, that a client claimed their account or that the management ended.

From the MCP server

Ten MCP tools mirror the provider's operations. Each one needs the scope of the operation it mirrors, which is always privileged: use an API key, because OAuth does not carry accounts:*.

ToolScopeOperation
provision_managed_accountaccounts:writePOST /v1/accounts
list_managed_accountsaccounts:readGET /v1/accounts
get_managed_accountaccounts:readGET /v1/accounts/{account}
get_provisioning_usageaccounts:readGET /v1/accounts/{account}/provisioning-usage
reissue_managed_account_claimaccounts:writePOST /v1/accounts/{account}/claim-tokens
change_managed_account_access_levelaccounts:writePATCH /v1/accounts/{account}/access-level
end_account_managementaccounts:writeDELETE /v1/accounts/{account}/management
change_managed_account_planaccounts:writePATCH /v1/accounts/{account}/plan
preview_managed_account_plan_changeaccounts:readGET /v1/accounts/{account}/plan/preview
preview_managed_account_provisioningaccounts:readGET /v1/accounts/provisioning-preview

The bulk import has no MCP tool: it reads uploaded files, which the MCP server does not carry. MCP tools lists the rest of the catalog.

Next steps

On this page

Need a hand?Contact support