Factuarea APIDevelopers

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:

AspectHow it works
Environmenttest. It is the environment of the key that created it, it never changes, and a test account never becomes a live one.
Who reaches itThe 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 IDThe tax ID you give it, as a sandbox company with no real company behind it.
Statusprovisioned for ever. It has no holder, no voucher, and nobody claims it.
CostNone. 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.
EffectsNone 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:read and accounts:write are 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-keys with environment set to test. Give it accounts:read and accounts:write, plus webhooks:read for step 5: it is the only webhook scope a test key can carry.
  • An Idempotency-Key on 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 answers 422 idempotency_key_required.

The path in one table

StepOperationWhat a test key gets
1. Read your accountGET /v1/me200. Your real account, the key with environment test, and your test accounts in client_accounts.
2. Provision a test accountPOST /v1/accounts201 with environment test and no voucher.
3. Import a test filePOST /v1/accounts/imports/preview and POST /v1/accounts/imports200 and 201. The managed_account rows create test accounts, and the own_nif rows come out ERROR.
4. Operate the test accountGET /v1/accounts/{account}, PATCH …/access-level, PATCH …/plan and DELETE …/management200, with no charge.
5. Read its eventsGET …/webhooks and GET …/deliveries200. 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.environment is test and account.environment is live. The scope of the response, not shown here, lists the sandbox company the key reaches, and only that one.
  • is_provider and privileged_scopes tell you whether you hold the provider privilege. Without it the provisioning of step 2 answers 403 privileged_scope_required.
  • client_accounts lists only your test accounts. It needs accounts:read in effect on the key, or it comes back empty with client_accounts_visible set to false. 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_url and claim_expires_at are null, and no email goes out. Leave send_email out: a test account has no voucher to deliver.
  • owner_email is 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-usage counts it in managed_accounts.test, and limit and remaining do 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 answers 200 with 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:

CodeStatusWhen
privileged_scope_required403Your 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_value422A field fails validation, for example a tax_id without a valid control character. error.param names it.
managed_account_plan_not_self_serve422plan is enterprise, which only we assign by contract.
managed_account_external_ref_in_other_environment409The external_ref names a live account.
idempotency_key_required422The 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_account rows are VALID and would create test accounts, with no voucher.
  • The own_nif row is ERROR with code live_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.

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.

  1. 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.
  2. Open Settings → Developers → Webhooks and choose Account in the Company | Account switch.
  3. 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_provisioned for step 2, account.import_completed for step 3, and account.access_level_changed and account.management_ended for 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 pending with attempt set to 0. 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 tryWhat a test key getsWhat to do
A write on your own live account: team, invitations, ownership, API keys, tax ID portfolio, tax ID transfers or employee seats403 live_credential_required, with nothing writtenUse a fact_live_ key. The reads of your account work with the test key.
A write on a live managed account404 account_not_foundA 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 delivery403: 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 rowThe row comes out ERROR with code live_credential_required, creates nothing and adds 0 to the totalsImport those rows with a live key.
Reissue the voucher of a test account, or claim it422 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 liveNo 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 errorNothing 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 certificateThe 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 API404 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 needs owner_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 answers 409 from 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.

On this page

Need a hand?Contact support