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.
status | What it means |
|---|---|
provisioned | You created it and nobody has claimed it yet, so it has no holder. Your account pays for it. |
claimed | The 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. |
active | The client runs it and pays for it: they took over the payment, or the management ended after the claim. |
management.status | What it means |
|---|---|
active | Your account manages it. |
ended | The 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*gets403 insufficient_scopeon these operations. Listaccounts:readandaccounts:writeexplicitly, 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/metells you whether you have it, withaccount.is_providerandaccount.privileged_scopes. A key can only carryaccounts:*while that list names them, so creating such a key without the privilege answers403 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.
| Field | Required | Notes |
|---|---|---|
external_ref | Yes | Your own reference for the account, up to 128 characters, unique for your account. It makes the call idempotent. |
plan | Yes | emprendedor, empresario or integral. enterprise is only assigned by us, by contract: 422 managed_account_plan_not_self_serve. |
tax_id | Yes | The client's NIF, CIF or NIE, up to 20 characters, validated with its control character. |
legal_name | Yes | Legal name, 2 to 100 characters. Without display_name, it is also the name of the account. |
access_level | No | The level your account keeps on it: none (the default), view or operate. |
display_name | No | Visible name of the account and of its tax ID, 2 to 120 characters. |
address, city, postal_code, province, country | No | The fiscal address of the client. The postal code derives the AEAT zone. |
language | No | es (the default), en or ca. The language of the account and of its voucher. |
owner_email | Depends | The 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_email | No | false 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.
| Code | Status | When |
|---|---|---|
insufficient_scope | 403 | The key does not list accounts:write. * does not count. |
privileged_scope_required | 403 | Your account does not hold the privilege, or the person who issued the key is no longer an owner or admin of it. |
invalid_param_value | 422 | A field fails validation. error.param names it. |
managed_account_plan_not_self_serve | 422 | plan is enterprise. |
managed_account_provider_in_trial | 422 | Your account is in its trial and asked for a live account, which is charged. During the trial, only test accounts. |
managed_accounts_limit_reached | 422 | Your account is at its cap of live managed accounts. |
business_rule_violation with subcode nif_already_claimed_by_another_account | 422 | The 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_required | 402 | A live account needs a payment method on your account, and there is none. Add one and repeat the call. |
provisioning_account_claimed | 409 | The external_ref names an account its holder already claimed. |
managed_account_external_ref_in_other_environment | 409 | The external_ref is used by an account of the other environment. |
idempotency_key_required | 422 | The request carries no Idempotency-Key. |
idempotency_key_reused | 422 | That 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 environment | 200 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 environment | 409 managed_account_external_ref_in_other_environment, with nothing written. Use another reference. |
| An account whose management you ended before anyone claimed it | 200 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 claimed | 409 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:
| Field | What it is |
|---|---|
claim_token | The single-use secret. We keep only its fingerprint, never the voucher itself. |
claim_url | The link to the claim page in the app. It carries the token. |
claim_expires_at | When 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:
| Call | claim_token and claim_url |
|---|---|
New live account, send_email: false (the default) | Returned, once, in the 201. |
New live account, send_email: true | null. The link travels only in the email to owner_email. |
| New test account | null. A test account has no voucher. |
external_ref already provisioned (200) | null. No voucher is issued. |
| Idempotent replay | null. |
| List and detail | Never 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 carriesclaim_urlonce and you deliver it through your own channel, for example your CRM or your own email.owner_emailis still required with a live key, because the voucher is issued in its name.send_email: true. We email the link toowner_email, in thelanguageof the account, and the response carriesnullinclaim_tokenandclaim_url.owner_emailis 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.
| Code | Status | When |
|---|---|---|
provisioning_account_claimed | 409 | The holder already claimed the account. There is nothing left to reissue. |
managed_account_not_claimable | 422 | It is a test account, and a test account is never claimed. |
idempotency_key_required | 422 | The request carries no Idempotency-Key. |
account_not_found | 404 | {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.
| Parameter | Notes |
|---|---|
status | provisioned, 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_ref | Your reference, an exact match: zero or one result. Up to 128 characters. |
limit | Between 1 and 100, 25 by default. |
starting_after | The 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.
| Field | Meaning |
|---|---|
id | The {account} of the routes that name a managed account. |
external_ref | Your own reference. |
status | provisioned, claimed or active, as above. |
environment | live or test: the environment of the key that provisioned it. A test account is never charged and cannot be claimed. |
plan, pending_plan | The current plan. pending_plan is a downgrade that takes effect at the end of the period, or null. |
billing_interval | monthly: managed accounts are billed monthly. |
access_level | The level your account keeps on it. |
display_name, language | Its visible name, and the language of the account and of its voucher. |
companies | Its tax IDs, each with id (the {company} of the tax ID axis), tax_id and name. |
claimed, claimed_at | Whether the holder claimed it, and when. |
management | status (active or ended) and ended_at. |
paid_by_provider | true while your account pays for it, false once the holder took over the payment. |
created_at | When 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
}
}| Field | Meaning |
|---|---|
managed_accounts.live, managed_accounts.test | The 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, remaining | Your cap and the live accounts you can still provision. remaining is never negative. |
managed_accounts_by_plan | The live accounts by plan. It always lists every plan of the catalog, including those at 0. |
provider_paid_additional_nifs, provider_paid_employee_seats | What 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
ownerof the account.claimedistrue,claimed_atis set andstatusmoves fromprovisionedtoclaimed. If you had already ended the management, the account goes straight toactive. - 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_provideristrue, 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.
| Level | What your credentials reach |
|---|---|
none | Nothing 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. |
view | The tax IDs of the account, read-only. A write on one of them answers 403 company_read_only. |
operate | The 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
provisionedand is not operable: all its tax IDs becomeinactive, withsuspension_reason: "management_ended". The voucher keeps working. The account stays that way until the client claims it, or until you provision the sameexternal_refagain, which reactivates the management. - After the claim. The account becomes
activeand 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_requiredand 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.
| Routes | Managed account |
|---|---|
The provisioning routes: GET /v1/accounts/{account}, PATCH …/access-level, DELETE …/management, POST …/claim-tokens, PATCH …/plan and GET …/plan/preview | Accepted, 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-readiness | Accepted 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 logs | 404 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_reachedand 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 subcode | param | When |
|---|---|---|
verifactu_not_eligible with certificate_nif_mismatch | certificate_file | The tax ID of the certificate is not the one of the company. |
verifactu_not_eligible with certificate_expired | certificate_file | The certificate has expired or is not valid yet. |
business_rule_violation with invalid_certificate_password | certificate_password | The 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"
}
}| Blocker | What stands in the way |
|---|---|
tax_id_missing | The company profile has no tax ID. |
tax_id_invalid_format | The tax ID is present, but it is not a valid NIF, CIF or NIE. |
census_not_identified | The AEAT census check does not identify the holder of the tax ID. |
census_revoked | The AEAT census check reports the tax ID as revoked. |
census_unavailable | The census check is not available, so we cannot say the tax ID can issue. |
fiscal_profile_incomplete | Fiscal data that issuing needs is missing: address, legal name or regime. |
no_default_series | The tax ID has no default series for invoices. |
company_not_operable | The tax ID is in your scope, but it is not operable, for example because it is inactive. |
module_unavailable | A module that issuing needs is not granted. missing_modules names it. |
verifactu_certificate_missing | VeriFactu 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:*.
| Tool | Scope | Operation |
|---|---|---|
provision_managed_account | accounts:write | POST /v1/accounts |
list_managed_accounts | accounts:read | GET /v1/accounts |
get_managed_account | accounts:read | GET /v1/accounts/{account} |
get_provisioning_usage | accounts:read | GET /v1/accounts/{account}/provisioning-usage |
reissue_managed_account_claim | accounts:write | POST /v1/accounts/{account}/claim-tokens |
change_managed_account_access_level | accounts:write | PATCH /v1/accounts/{account}/access-level |
end_account_management | accounts:write | DELETE /v1/accounts/{account}/management |
change_managed_account_plan | accounts:write | PATCH /v1/accounts/{account}/plan |
preview_managed_account_plan_change | accounts:read | GET /v1/accounts/{account}/plan/preview |
preview_managed_account_provisioning | accounts:read | GET /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
what you pay for each managed account, how a plan change is charged and what happens when the client takes over.
run the whole provisioning flow with a test key, with no charge and no claim.
preview and apply a CSV of clients in one go.
receive the events of your account and of its tax IDs at one endpoint.
the account you operate yourself, its team and GET /v1/me.
when a client lets your account reach some of its tax IDs without you provisioning it.
every code of the account model, with its cause and what to do.