Factuarea API

Managed companies

Register, provision and operate child companies under your master tenant — the gestoría model over the v1 API, with per-seat billing and an active/inactive lifecycle.

A managed company is a child sub-account you create and operate under your own master tenant. This is the gestoría model: one accounting firm (the master) holds a single set of credentials and, through them, registers and manages many client companies, each isolated from the others.

You provision each child company, and then drive it in one of two ways: mint a child API key scoped to it, or keep your master key and switch target company per request with the X-Active-Profile header. This page covers the companies themselves — creating, provisioning, the active/inactive lifecycle, seat billing and archiving.

Eleven endpoints under /v1/companies manage the companies.

OperationEndpointScope
List companiesGET /v1/companiescompanies:read
Create a companyPOST /v1/companiescompanies:write
Retrieve a companyGET /v1/companies/{id}companies:read
Update a companyPATCH /v1/companies/{id}companies:write
Archive a companyDELETE /v1/companies/{id}companies:delete
Poll creation statusGET /v1/companies/{id}/creation-statuscompanies:read
Verify (reconcile) creationPOST /v1/companies/{id}/verify-creationcompanies:write
Deactivate a companyPOST /v1/companies/{id}/deactivatecompanies:write
Reactivate a companyPOST /v1/companies/{id}/activatecompanies:write
Activate companies in batchPOST /v1/companies/activatecompanies:write
Preview the seat chargeGET /v1/companies/seat-charge-previewcompanies:read

{id} is the company's id — an opaque UUID v7, not its tax_id. See the full schemas in the API Reference.

Create a managed company

POST /v1/companies registers a new child company under your master tenant. name and tax_id are the only required fields; the rest of the profile (legal name, fiscal address, contact details) is optional and can be sent in the same request. The tax_id (NIF / CIF / NIE) must be unique across the companies you already manage; a duplicate returns 409. Requires the companies:write scope.

curl -X POST https://api.factuarea.com/v1/companies \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Talleres García SL",
    "tax_id": "B12345678"
  }'

Response (201):

{
  "data": {
    "object": "company",
    "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c",
    "name": "Talleres García SL",
    "business_name": "Talleres García, Sociedad Limitada",
    "tax_id": "B12345678",
    "status": "active",
    "address": "Calle Mayor 1",
    "city": "Madrid",
    "postal_code": "28013",
    "province": "Madrid",
    "country_aeat_zone": "peninsula",
    "email": "contacto@talleresgarcia.es",
    "phone": null,
    "logo_url": null,
    "created_at": "2026-01-15T09:30:00+00:00",
    "updated_at": null
  }
}

Request body

FieldRequiredNotes
nameyesCommercial name (1–255 chars).
tax_idyesSpanish fiscal id (NIF / CIF / NIE). Immutable after creation.
business_namenoLegal name (razón social), up to 100 chars.
addressnoFiscal address.
citynoCity of the fiscal seat.
postal_codenoPostal code — the AEAT zone (country_aeat_zone in responses) is derived from it.
provincenoProvince.
countrynoCountry.
emailnoContact email.
phonenoContact phone.

The status field in the response is the company's active/inactive lifecycle, a different axis from the provisioning_status that tracks the async setup. There is no country_aeat_zone input field — you send a free-text country, and the AEAT zone is derived from the postal_code.

Only form-level validation runs at creation. The AEAT census check is a separate provisioning step — registering a company here does not verify it against the census in the same request.

Duplicate tax id

Reusing a tax_id you already manage returns 409 with resource_already_exists, and existing_resource_id points at the company that already holds it:

{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_already_exists",
    "message": "Ya gestionas una empresa con este NIF.",
    "param": "tax_id",
    "existing_resource_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c"
  }
}

The tax_id is unique per master tenant, not globally: two different gestorías may each manage a company with the same fiscal id.

Provisioning lifecycle

Registering a real billing company is not instantaneous. POST /v1/companies returns right away, but behind it the child company is provisioned asynchronously: a default document series and the minimal fiscal config are set up, and — in live — the gestoría is charged for the new seat. Until that finishes the child is not yet operational. Two endpoints expose the lifecycle: one to poll it and one to reconcile it.

OperationEndpointScope
Poll creation statusGET /v1/companies/{id}/creation-statuscompanies:read
Verify (reconcile) creationPOST /v1/companies/{id}/verify-creationcompanies:write

Provisioning states

provisioning_status walks a small, one-way lifecycle:

StateMeaning
pendingThe child was registered; provisioning has not started yet.
awaiting_paymentThe gestoría has no payment method on file, so the seat cannot be charged yet. payment_setup_url points to where the master tenant adds one.
provisioningThe seat was charged (or the child is in test mode) and the tenant is being set up.
activeProvisioning finished. The child is fully operational.
failedProvisioning could not complete — failed_reason says why. Create the company again to retry.

With a test key (fact_test_) there is no Stripe call: the child goes straight to active, deterministically. The awaiting_payment and per-seat billing path only applies to live keys.

Per-seat billing

In live, each active child company is one seat charged inside the master tenant's existing subscription — a single recurring invoice that reads as "plan + N clients". Adding a child adds a seat and charges the prorated amount immediately for the remainder of the billing period; archiving a child removes the seat and credits the unused time to the next invoice. The child does not reach active until that immediate charge succeeds; if it fails, the child ends up failed. Preview the amount up front with the seat-charge preview.

Because the seat lives in the master tenant's own subscription, a child inherits the master's plan and add-ons, and an unpaid master subscription suspends the whole gestoría account — its children included. There is no separate invoice per child.

Poll the creation status

GET /v1/companies/{id}/creation-status returns the current provisioning_status and the timestamps. Poll it after creating a company until it reaches active (or failed). Requires the companies:read scope.

curl https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/creation-status \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

Response (200):

{
  "data": {
    "object": "company_creation_status",
    "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c",
    "provisioning_status": "active",
    "payment_setup_url": null,
    "failed_reason": null,
    "started_at": "2026-01-15T09:30:00+00:00",
    "completed_at": "2026-01-15T09:31:00+00:00"
  }
}

payment_setup_url is present only while awaiting_payment, and failed_reason only when failed; both are null otherwise.

Verify the creation

POST /v1/companies/{id}/verify-creation reconciles a child against the master tenant's subscription and advances it when it can. It takes no request body and is idempotent. Requires the companies:write scope.

curl -X POST \
  https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/verify-creation \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

Call it once the master tenant has added a payment method, or whenever you want to nudge a child out of awaiting_payment:

  • A child already active is a no-op — the call is safe to repeat.
  • While awaiting_payment, if the master now has a payment method, the prorated seat is charged and the child moves to active.
  • If the master still has no payment method, the call is a no-op with no error — the child stays awaiting_payment.

It returns the same creation-status resource as the poll endpoint, so you can read the resulting provisioning_status from the response directly.

The active/inactive lifecycle

Separate from provisioning, every child carries a status — its link lifecycle within the gestoría. It is the status field on the company resource, and it walks three states:

StateMeaning
activeThe child is linked and operational.
inactiveThe child is deactivated — unreachable until you reactivate it (paying its seat again), but its data is intact and the state is reversible.
archivedThe child has been unlinked. This is a terminal state.

Transitions are active ↔ inactive (deactivate / reactivate) and active → archived or inactive → archived (archive). archived is terminal.

Deactivating frees the seat; reactivating charges it again. This lets a gestoría park a client between engagements without losing its history, and bring it back later.

Deactivate a company

POST /v1/companies/{id}/deactivate moves an active child to inactive. The company becomes unreachable but keeps all its data, reversibly. It does not charge: the prorated credit for the freed seat is applied best-effort on the next invoice. Requires the companies:write scope.

curl -X POST \
  https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/deactivate \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

It returns the updated company resource with status: "inactive".

Reactivate a company

POST /v1/companies/{id}/activate moves an inactive child back to active. Reactivation is gated on an atomic seat charge: the prorated amount is charged first, and only if the charge succeeds does the child become active. If the master has no payment method, or the charge fails, the company stays inactive. Requires the companies:write scope.

curl -X POST \
  https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/activate \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

It returns the updated company resource with status: "active".

Activate companies in batch

POST /v1/companies/activate reactivates several children in one call, with a single combined charge — one invoice for the whole batch instead of one per company. The body takes company_ids, a list of child company id values (1–1000). Requires the companies:write scope.

curl -X POST https://api.factuarea.com/v1/companies/activate \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
  -H "Content-Type: application/json" \
  -d '{
    "company_ids": [
      "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c",
      "01931b3e-8d5b-7a1f-9c2d-5e6f7a8b9c0d"
    ]
  }'

The charge is atomic across the batch — all or nothing. Ownership (404 for a company outside your tree) and the inactive precondition (422) are validated for every company before any charge runs. The response is the list of reactivated companies ({ "data": [ … ] }).

Preview the seat charge

GET /v1/companies/seat-charge-preview returns what adding or reactivating child companies would cost, without charging anything. Use it to show the prorated amount before a POST /v1/companies or an activation, and to detect the "no payment method" case up front. Requires the companies:read scope.

It has two modes:

  • count (≥1, default 1) — previews the combined proration of activating that many children in one batch.
  • company_ids — a list of specific child id values, for a coverage-aware preview: the amount is 0 with already_covered: true when they are all still covered this period, otherwise it prorates only the uncovered ones. When present, it takes precedence over count.
curl "https://api.factuarea.com/v1/companies/seat-charge-preview?count=1" \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

Response (200):

{
  "data": {
    "object": "seat_charge_preview",
    "amount": 1240,
    "tax_amount": 260,
    "total": 1500,
    "tax_rate": 21,
    "currency": "EUR",
    "next_invoice_date": "2026-02-01",
    "requires_payment_method": false,
    "requires_active_plan": false,
    "included_in_trial": false,
    "already_covered": false,
    "is_first_seat": false,
    "recurring_quantity": 4,
    "recurring_base_cents": 4000,
    "recurring_total_cents": 4840
  }
}

amount is the proration's taxable base in the currency's minor units (cents), tax_amount the VAT, and total (amount + tax_amount) what is actually charged. tax_rate is the derived VAT percentage (e.g. 21) or null if Stripe Tax did not compute it. The recurring_* fields project the combined monthly fee after activation: total seat count, base without VAT, and total with VAT (recurring_total_cents is null when VAT is not computable).

Four mutually exclusive flags explain a 0 amount, in priority order:

Flagamount is 0 because…
already_coveredThe companies you would activate are already included in this period's subscription — reactivation is free.
requires_active_planThe gestoría has no active plan and must contract one before managing companies.
included_in_trialThe gestoría is in its trial — the company is created for free (seats start billing when the trial converts to a paid plan).
requires_payment_methodThe gestoría has a paid plan but no payment method on file, and must add one (Billing Portal) first.

is_first_seat is true when the activation creates the master's first seat subscription: the charge is a full month and today anchors the monthly billing day of the combined cycle.

List and retrieve companies

GET /v1/companies returns your managed companies with cursor pagination; GET /v1/companies/{id} returns one. Both are scoped to your master tenant.

curl https://api.factuarea.com/v1/companies?limit=25 \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

A company managed by a different master tenant returns 404, never 403 — the API never reveals that a company you cannot manage exists.

Update a company

PATCH /v1/companies/{id} is a partial update. The only editable field is name — the profile (legal name, fiscal address, contact, AEAT zone) is not editable here, and the tax_id is immutable and is rejected if included in the body. A company must be active to be edited. Requires the companies:write scope.

curl -X PATCH \
  https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Talleres García e Hijos SL" }'

Archive a company

DELETE /v1/companies/{id} archives the company rather than deleting it: its status moves to archived and it stops accepting operations. An active or inactive company can be archived; archived is terminal. Requires the companies:delete scope.

curl -X DELETE \
  https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c \
  -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z"

Archiving may be blocked: if the company still has state that prevents it (for example outstanding documents), the request returns 422 and the company keeps its current status. Resolve the blocking condition first, then archive.

Scopes and isolation

Companies are gated by their own scopes:

  • companies:read — list and retrieve managed companies, poll creation status, and preview the seat charge.
  • companies:write — create, update, activate and deactivate managed companies, and verify their creation.
  • companies:delete — archive managed companies.

Every operation is scoped to your master tenant. A company id belonging to a different master returns 404 — never 403. This cross-master isolation is the core guarantee of the gestoría model: a master can only ever see and act on its own companies. The same guarantee governs acting on behalf of a child and its API keys.

On this page