Test managed accounts with fact_test_
Run the whole provisioning flow with a test key: test accounts, import, reads and events, with no charge and no claim.
You can rehearse the whole provisioning of client accounts with a fact_test_ key before you touch a real client. Nothing is charged, no voucher or email goes out, no real company is involved and nothing reaches VeriFactu. This guide walks through the path in five steps, from reading your own account to reading the events of a test account, and closes with what the path does not let you do, so you know what to try with a live key.
It assumes you provision accounts for your clients as managed accounts describes. How a test key behaves on the rest of the API is in test mode and sandbox.
What a test account is
A test account is a managed account created with a fact_test_ key. It is a normal managed account in the test environment, with these differences:
| Aspect | How it works |
|---|---|
| Environment | test. It is the environment of the key that created it, it never changes, and a test account never becomes a live one. |
| Who reaches it | The fact_test_ keys of your account and the web app in Test mode. A live key answers 404 account_not_found, the same as for an account that is not yours. |
| Tax ID | The tax ID you give it, as a sandbox company with no real company behind it. |
| Status | provisioned for ever. It has no holder, no voucher, and nobody claims it. |
| Cost | None. No call to the payment provider, no payment method asked, and it does not count against your cap of managed accounts. paid_by_provider reads true because your account is its payer, but nothing is charged. |
| Effects | None in the real world. The sandbox company sends no VeriFactu record and no real email. |
Before you start
You need three things:
- The provider privilege.
accounts:readandaccounts:writeare privileged scopes that we grant account by account, also for test accounts: see privileged scopes. An account in its trial can create test accounts, and only those. - A test key. Create it in Settings → Developers → API Keys choosing the Test environment, or with a live key through
POST /v1/accounts/{account}/api-keyswithenvironmentset totest. Give itaccounts:readandaccounts:write, pluswebhooks:readfor step 5: it is the only webhook scope a test key can carry. - An
Idempotency-Keyon the irreversible writes. Provisioning, applying an import, changing the plan and ending the management need one with a test key too, and a call without it answers422 idempotency_key_required.
The path in one table
| Step | Operation | What a test key gets |
|---|---|---|
| 1. Read your account | GET /v1/me | 200. Your real account, the key with environment test, and your test accounts in client_accounts. |
| 2. Provision a test account | POST /v1/accounts | 201 with environment test and no voucher. |
| 3. Import a test file | POST /v1/accounts/imports/preview and POST /v1/accounts/imports | 200 and 201. The managed_account rows create test accounts, and the own_nif rows come out ERROR. |
| 4. Operate the test account | GET /v1/accounts/{account}, PATCH …/access-level, PATCH …/plan and DELETE …/management | 200, with no charge. |
| 5. Read its events | GET …/webhooks and GET …/deliveries | 200. Deliveries recorded, pending, never sent. |
1. Read your own account
A fact_test_ key belongs to your real account, so it reads it as a production key does. Start by asking what the key is. GET /v1/me needs no scope:
curl https://api.factuarea.com/v1/me \
-H "Authorization: Bearer $FACTUAREA_TEST_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": {
"name": "Pruebas de integración",
"prefix": "fact_test_1OSf9KdP",
"scopes": [
"accounts:read",
"accounts:write",
"webhooks:read"
],
"environment": "test"
},
"client_accounts": [],
"client_accounts_visible": true,
"client_accounts_has_more": false
}
}The response is shortened. Read it like this:
- The key is a test one, the account is real.
api_key.environmentistestandaccount.environmentislive. Thescopeof the response, not shown here, lists the sandbox company the key reaches, and only that one. is_providerandprivileged_scopestell you whether you hold the provider privilege. Without it the provisioning of step 2 answers403 privileged_scope_required.client_accountslists only your test accounts. It needsaccounts:readin effect on the key, or it comes back empty withclient_accounts_visibleset tofalse. The example is empty because no test account exists yet, and after step 2 it lists the new one. Your live accounts do not show up here: live keys list them.
The other reads of your own account answer with a test key as they do with a production key, each with the scope of its operation and with the real data of your account: provisioning-usage, the tax ID portfolio, the members and the usage. The webhooks and the request logs of the account answer with those of your test environment.
A test key reads the real data of your account, such as its members and its tax IDs, and never changes it. Every write on your own account answers 403 live_credential_required: see what the path does not allow.
2. Provision a test account
Provision with POST /v1/accounts and a test key. It is the same call as in live, with the same body, and the environment is the one of the key: it never goes in the body. It needs accounts:write and an Idempotency-Key.
curl -X POST https://api.factuarea.com/v1/accounts \
-H "Authorization: Bearer $FACTUAREA_TEST_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"external_ref": "test-0001",
"plan": "empresario",
"access_level": "operate",
"tax_id": "B12345674",
"legal_name": "Talleres García SL"
}'{
"data": {
"id": "0199b2a0-3e44-7c31-8d10-4b1a2c3d4e01",
"object": "managed_account",
"external_ref": "test-0001",
"status": "provisioned",
"environment": "test",
"plan": "empresario",
"pending_plan": null,
"billing_interval": "monthly",
"access_level": "operate",
"display_name": "Talleres García SL",
"language": "es",
"companies": [
{
"id": "0199b2a0-3e44-7c31-8d10-4b1a2c3d4e02",
"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-30T10:00:00+00:00",
"claim_token": null,
"claim_url": null,
"claim_expires_at": null
}
}The answer is 201 Created with Location: /v1/accounts/{id}. What to expect, compared with a live account:
- No voucher.
claim_token,claim_urlandclaim_expires_atarenull, and no email goes out. Leavesend_emailout: a test account has no voucher to deliver. owner_emailis optional. A test account has no holder to invite.- The tax ID is validated as in live. It needs a valid NIF, CIF or NIE with its control character.
- It does not count against your cap.
provisioning-usagecounts it inmanaged_accounts.test, andlimitandremainingdo not move. The trial, cap and payment method checks of a live account do not run, so a provider in its trial can provision test accounts. - The call is idempotent by
external_ref. Repeating it answers200with the same account and creates nothing.
The external_ref belongs to your account in both environments. Reusing one that a live account has answers 409 managed_account_external_ref_in_other_environment, and the other way round. Keep your test references apart from your live ones, for example with a test- prefix.
The errors you can meet here are the ones of provisioning a live account that do not depend on a charge:
| Code | Status | When |
|---|---|---|
privileged_scope_required | 403 | Your account does not hold the provider 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, for example a tax_id without a valid control character. error.param names it. |
managed_account_plan_not_self_serve | 422 | plan is enterprise, which only we assign by contract. |
managed_account_external_ref_in_other_environment | 409 | The external_ref names a live account. |
idempotency_key_required | 422 | The request carries no Idempotency-Key. |
3. Import a test file
The bulk import works with a test key in two calls: a preview with no effects and the application. The environment is, again, the one of the key. Try it with a file of three rows, separated by semicolons and with the columns of the template:
mode;external_ref;plan;tax_id;legal_name;owner_email
managed_account;test-0002;emprendedor;A58818501;Bodega Sol SL;
managed_account;test-0003;empresario;B28013829;Horno Mar SL;hugo@hornomar.example
own_nif;;;B76543214;Delegación Este SL;Preview it first. The preview is a POST because it uploads the file, it writes nothing and it needs no Idempotency-Key:
curl -X POST https://api.factuarea.com/v1/accounts/imports/preview \
-H "Authorization: Bearer $FACTUAREA_TEST_KEY" \
-F "accounts_file=@clients.csv"{
"data": {
"object": "managed_account_import_preview",
"rows": [
{
"line": 2,
"mode": "managed_account",
"external_ref": "test-0002",
"tax_id": "A58818501",
"status": "VALID",
"code": null,
"errors": {},
"plan": "emprendedor",
"monthly_amount_cents": 0,
"proration_amount_cents": 0
},
{
"line": 3,
"mode": "managed_account",
"external_ref": "test-0003",
"tax_id": "B28013829",
"status": "VALID",
"code": null,
"errors": {},
"plan": "empresario",
"monthly_amount_cents": 0,
"proration_amount_cents": 0
},
{
"line": 4,
"mode": "own_nif",
"external_ref": null,
"tax_id": "B76543214",
"status": "ERROR",
"code": "live_credential_required",
"errors": {},
"plan": null,
"monthly_amount_cents": 0,
"proration_amount_cents": 0
}
],
"totals": {
"total_rows": 3,
"valid": 2,
"warning": 0,
"already_exists": 0,
"blocked": 0,
"error": 1
},
"cost": {
"currency": "EUR",
"total": {
"monthly_amount_cents": 0,
"proration_amount_cents": 0
}
},
"own_company_customers": null
}
}The response is shortened: the rows carry more fields and cost also splits by mode. With a test key:
- The
managed_accountrows areVALIDand would create test accounts, with no voucher. - The
own_nifrow isERRORwithcodelive_credential_required: a tax ID of your own account is registered with a live key only. The options row of the import, the one that applies the customers to your own company, comes out the same. - The cost is
0. The total counts neither the monthly fee nor the proration, and no call reaches the payment provider.
Apply the same file with POST /v1/accounts/imports. It needs an Idempotency-Key:
curl -X POST https://api.factuarea.com/v1/accounts/imports \
-H "Authorization: Bearer $FACTUAREA_TEST_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "accounts_file=@clients.csv"The answer is 201 Created with the result of the execution, shortened here:
{
"data": {
"id": "0199b2a0-5f60-7d40-9e20-6d3c4e5f6a05",
"object": "managed_account_import",
"status": "completed",
"stats": {
"total_rows": 3,
"valid": 2,
"warning": 0,
"already_exists": 0,
"blocked": 0,
"error": 1,
"created": 2,
"reconciled": 0,
"customers_queued": 0,
"monthly_amount_cents": 0,
"proration_amount_cents": 0
},
"rows": [
{
"line": 2,
"mode": "managed_account",
"external_ref": "test-0002",
"status": "VALID",
"account_id": "0199b2a0-5f66-7d42-9e21-5c2b3d4e5f03",
"claim_token": null,
"claim_url": null
}
],
"own_company_customers": null
}
}Two accounts are created and the own_nif row creates nothing. No row carries a voucher. Apply the same file again to see the reconciliation: the two accounts come out ALREADY_EXISTS and nothing is created twice. The columns, the limits and the row statuses are the ones of the bulk import.
4. Operate the test account
The provisioning operations name the test account by its id in the {account} segment. With a test key, your account reaches the test accounts it provisioned and no other.
- Read it.
GET /v1/accounts/{account}answers200with the account as in step 2, andcompanies[]lists its tax ID.GET /v1/accountslists only test accounts with a test key. A live key answers404 account_not_foundon theidof a test account. - Change your level.
PATCH /v1/accounts/{account}/access-levelwithlevelanswers200with the newaccess_level. A test account is never claimed, so you can raise and lower the level as often as you want. - Change the plan.
GET /v1/accounts/{account}/plan/previewwithplanshows both amounts at0, andPATCH /v1/accounts/{account}/planchanges the plan at once with no charge. Both ask for theoperatelevel, and the change needs anIdempotency-Key. Managed account pricing explains the live flow. - End the management.
DELETE /v1/accounts/{account}/managementanswers200withmanagement.statusset toended, and from your next request the account answers404like a live one does: see end the management.
curl -X PATCH https://api.factuarea.com/v1/accounts/0199b2a0-3e44-7c31-8d10-4b1a2c3d4e01/access-level \
-H "Authorization: Bearer $FACTUAREA_TEST_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "level": "view" }'The data of the tax ID of a test account, such as its invoices and its contacts, is not reachable with a key. A fact_test_ key reaches only the sandbox company of your own account, so /v1/companies/{company}/… with the id of companies[] answers 404 profile_not_found. To work with that data, switch the web app to Test mode with the test mode switch of the header: your test accounts appear among your client accounts, and choosing one makes its sandbox company the active one.
5. Read its events in a test endpoint
The events of a test account never reach a live endpoint, and an endpoint of the test environment never receives the events of a live account. To follow the path you need a test endpoint of your account, and a test key cannot create one: you create it in the web app.
- Switch the web app to Test mode with the test mode switch of the header. You need to be an owner or admin of the account.
- Open Settings → Developers → Webhooks and choose Account in the Company | Account switch.
- Choose New account endpoint. Write an HTTPS URL, leave Accounts it receives events from on All, and pick the events you want under Subscribed events:
account.managed_account_provisionedfor step 2,account.import_completedfor step 3, andaccount.access_level_changedandaccount.management_endedfor step 4. The signing secret is shown once.
The endpoint takes the environment of the session: it is test, it never changes and it is invisible from live. The number of endpoints depends on the plan and is counted per account and environment. When the plan has none, or you are at the limit, the app refuses the endpoint with addon_required or account_webhook_endpoint_limit_reached: see account webhooks.
Now repeat steps 2 to 4 with your test key and read what happened. With webhooks:read, a test key reads the endpoints and deliveries of the test environment:
curl https://api.factuarea.com/v1/accounts/$ACCOUNT_ID/webhooks \
-H "Authorization: Bearer $FACTUAREA_TEST_KEY"The answer lists your test endpoints, and only those. Take the id of yours and list its deliveries with GET /v1/accounts/{account}/webhooks/{webhook}/deliveries, where {account} is your own account.id:
{
"data": [
{
"id": "0199b2a0-3e80-7c55-8d41-8f5e6a7b8c07",
"object": "account_webhook_delivery",
"event_id": "0199b2a0-3e7a-7c50-8d40-9a6f7b8c9d08",
"event_type": "account.managed_account_provisioned",
"event_source": "account",
"company_id": null,
"account_relationship": "own",
"environment": "test",
"status": "pending",
"attempt": 0,
"manual_retry_count": 0,
"next_retry_at": null,
"response_status": null,
"response_body": null,
"duration_ms": null,
"payload": {
"id": "0199b2a0-3e7a-7c50-8d40-9a6f7b8c9d08",
"type": "account.managed_account_provisioned",
"livemode": false,
"account": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a00"
},
"account_relationship": "own",
"company_id": null,
"data": {
"type": "account.managed_account_provisioned",
"object": {
"object": "account_relation",
"id": "0199b2a0-3e46-7c33-8d12-0b7a8c9dae09",
"origin": "provisioned",
"level": "operate",
"previous_level": null,
"grantor_account_id": "0199b2a0-3e44-7c31-8d10-4b1a2c3d4e01",
"grantee_account_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a00",
"status": "active",
"managed_account_id": "0199b2a0-3e44-7c31-8d10-4b1a2c3d4e01",
"external_ref": "test-0001"
}
}
},
"created_at": "2026-09-30T10:00:01+00:00"
}
],
"has_more": false,
"next_cursor": null
}The delivery is shortened. Its payload is the envelope that a live endpoint would receive, with livemode set to false. What makes a test endpoint different:
- Nothing is sent to your URL. The delivery of a test endpoint is recorded and never leaves Factuarea, so it stays
pendingwithattemptset to0. You read it here, or in the endpoint detail of the web app, and there is no request to verify. - Only the events of a test account arrive. The environment of an
account.*event is the one of the account it is about. The events of a live account never reach this endpoint, and a live endpoint never receives the events of your test accounts.
Read one delivery with GET …/deliveries/{delivery}, which returns it with the same payload. The reads of the account webhooks keep their guards with a test key: the person who issued the key must be an owner or admin of the account today, as account webhooks explains.
What the path does not allow
| You try | What a test key gets | What to do |
|---|---|---|
| A write on your own live account: team, invitations, ownership, API keys, tax ID portfolio, tax ID transfers or employee seats | 403 live_credential_required, with nothing written | Use a fact_live_ key. The reads of your account work with the test key. |
| A write on a live managed account | 404 account_not_found | A test key reaches only test accounts, and a live key reaches only live ones. |
| Create, edit, test or delete an account webhook endpoint, rotate its secret or retry a delivery | 403: insufficient_scope because a test key cannot carry webhooks:write, or module_not_available_in_sandbox if it carries it anyway. The MCP answers live_credential_required. | Create the test endpoint in the web app in Test mode, as in step 5. |
The own_nif rows of an import, and its options row | The row comes out ERROR with code live_credential_required, creates nothing and adds 0 to the totals | Import those rows with a live key. |
| Reissue the voucher of a test account, or claim it | 422 managed_account_not_claimable. A test account has no voucher, no claim link and no holder. | Nothing: it stays provisioned for ever. |
| Promote a test account to live | No operation does it, and a test account never changes its environment. | Provision a live account with a fact_live_ key and a different external_ref. Nothing carries over. |
| See a charge, a proration or a payment error | Nothing is charged, no payment method is asked, and the plan preview shows 0. The checks of your trial, of your cap of managed accounts and of your payment method do not run. | Test the charge with a live account: see pricing and payer change. |
| Submit to VeriFactu, or upload the client's certificate | The sandbox company has no fiscal effects and the VeriFactu module is not available in test mode. No real email goes out either. | Follow the client's VeriFactu certificate with a live account. |
| Work with the data of the tax ID of a test account through the API | 404 profile_not_found on /v1/companies/{company}/… | Use the web app in Test mode, as in step 4. |
Moving to production
A test account and a live account are independent, and nothing you created with the test key moves to production. To go live:
- Provision the live accounts with a
fact_live_key. A live account needsowner_email, a payment method in your account and room under your cap, and it is charged prorated at once: see pricing and payer change. - Give each one a new
external_ref. The one of a test account answers409from the live side. - Create a live endpoint for the live events. The test endpoint never receives them. Create it with a live key or in the web app in Live mode.
Related
provision accounts for your clients, the claim voucher, your level and the end of the management.
what you pay for each live managed account and how a plan change is charged.
the columns, the modes, the row statuses and the limits of the import.
the endpoints, the events and the envelope of your account and of its tax IDs.
what a fact_test_ key does on the rest of the API.
every code of the account model, with its cause and what to do.