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.
| Operation | Endpoint | Scope |
|---|---|---|
| List companies | GET /v1/companies | companies:read |
| Create a company | POST /v1/companies | companies:write |
| Retrieve a company | GET /v1/companies/{id} | companies:read |
| Update a company | PATCH /v1/companies/{id} | companies:write |
| Archive a company | DELETE /v1/companies/{id} | companies:delete |
| Poll creation status | GET /v1/companies/{id}/creation-status | companies:read |
| Verify (reconcile) creation | POST /v1/companies/{id}/verify-creation | companies:write |
| Deactivate a company | POST /v1/companies/{id}/deactivate | companies:write |
| Reactivate a company | POST /v1/companies/{id}/activate | companies:write |
| Activate companies in batch | POST /v1/companies/activate | companies:write |
| Preview the seat charge | GET /v1/companies/seat-charge-preview | companies: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
| Field | Required | Notes |
|---|---|---|
name | yes | Commercial name (1–255 chars). |
tax_id | yes | Spanish fiscal id (NIF / CIF / NIE). Immutable after creation. |
business_name | no | Legal name (razón social), up to 100 chars. |
address | no | Fiscal address. |
city | no | City of the fiscal seat. |
postal_code | no | Postal code — the AEAT zone (country_aeat_zone in responses) is derived from it. |
province | no | Province. |
country | no | Country. |
email | no | Contact email. |
phone | no | Contact 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.
| Operation | Endpoint | Scope |
|---|---|---|
| Poll creation status | GET /v1/companies/{id}/creation-status | companies:read |
| Verify (reconcile) creation | POST /v1/companies/{id}/verify-creation | companies:write |
Provisioning states
provisioning_status walks a small, one-way lifecycle:
| State | Meaning |
|---|---|
pending | The child was registered; provisioning has not started yet. |
awaiting_payment | The 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. |
provisioning | The seat was charged (or the child is in test mode) and the tenant is being set up. |
active | Provisioning finished. The child is fully operational. |
failed | Provisioning 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
activeis 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 toactive. - 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:
| State | Meaning |
|---|---|
active | The child is linked and operational. |
inactive | The child is deactivated — unreachable until you reactivate it (paying its seat again), but its data is intact and the state is reversible. |
archived | The 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 childidvalues, for a coverage-aware preview: the amount is0withalready_covered: truewhen they are all still covered this period, otherwise it prorates only the uncovered ones. When present, it takes precedence overcount.
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:
| Flag | amount is 0 because… |
|---|---|
already_covered | The companies you would activate are already included in this period's subscription — reactivation is free. |
requires_active_plan | The gestoría has no active plan and must contract one before managing companies. |
included_in_trial | The 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_method | The 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.