# Factuarea API — full documentation > Concatenated Markdown export of every page at https://docs.factuarea.com. Generated on demand from the source MDX (no UI components, no layout chrome). Each page begins with its title and canonical URL so LLMs can cite back to the live documentation. The API reference is rendered from the OpenAPI spec and, being identical in every language, appears once per operation. See `/llms.txt` for a curated index. Total pages: 1565. --- # Factuarea API (/) The Factuarea REST API exposes invoicing resources (clients, products, invoices, quotes, pro-forma invoices, delivery notes, recurring invoices, purchase invoices) over HTTPS with **API key** authentication. The entire public surface lives at [`https://api.factuarea.com/v1`](https://api.factuarea.com/v1) and returns JSON. Every resource is identified by an opaque `id` (a UUID v7 string). One copy-paste sequence against a `fact_test_` key: verify your key, grab a series and a tax, create a client, issue an invoice and send it. ## Quick start [#quick-start] **The API comes with your plan** The public API is **included in every Factuarea plan** — no beta program, no separate add-on. During the 10-day trial you already get API access on the `free` tier; paid plans raise the rate-limit tier. See [Rate limits](/guides/rate-limits). **Create your first API key** Open [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) and create a key with the scopes you need (for example `invoices:read,clients:read` to start). Copy the secret **only once** — you won't be able to see it again. Pick the **Test** environment to get a `fact_test_` key that operates on an isolated sandbox with no real-world effects. Build against it first, then create a `fact_live_` key to go to production. See [Test mode & sandbox](/guides/test-mode). **Verify your key** Before anything else, confirm the key works. `GET /v1/account` introspects the credential — it returns the company it belongs to, the plan, and the **scopes** and rate-limit **tier** of the key itself (needs `account:read`): ```bash curl https://api.factuarea.com/v1/account \ -H "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` ✅ You should see a `200` with an `account` snapshot: ```json { "data": { "object": "account", "company": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Acme Soluciones SL", "tax_id": "B12345678" }, "plan": { "slug": "empresario", "name": "Empresario" }, "api_key": { "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "name": "Sandbox integration", "prefix": "fact_test_3pXnR2Vb", "scopes": ["account:read", "clients:read", "invoices:read"], "tier": "starter" } } } ``` If you get `401 invalid_api_key`, re-check the value. The `scopes` array tells you exactly what this key can do — a later call that fails with `403 insufficient_scope` is missing one of them. **Make your first data request** Now list a real resource. `GET /v1/clients` returns a standard envelope with `data` (results), `has_more` and `next_cursor` ([cursor pagination](/guides/pagination)): ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Ready to issue your first invoice end-to-end? Follow the [Quickstart](/guides/quickstart). If you receive an error, look it up in [Errors](/guides/errors) by the returned `code`. **Configure webhooks (optional)** If your integration needs to react to events (invoice paid, quote accepted, etc.), configure a webhook endpoint signed with HMAC SHA256. See [Webhooks](/guides/webhooks). ## What the API covers [#what-the-api-covers] Full CRUD, search by tax ID, VIES validation. Products with prices, stock, SKU and tax rates. Invoices, quotes, pro-forma invoices, delivery notes, recurring invoices — with lines, retentions and equivalence surcharge. Send by email, mark as paid/accepted, generate PDF, void, create corrective invoice, convert between types. Vendor bills with PDF upload, mark\_paid, mark\_received. Legal numbering series per document type (read-only via API to guarantee tax continuity). FacturaE 3.2.2 XML download and FACe submissions — submit, track the processing status and request cancellations. Employees, work schedules, the time-clock ledger, monthly closes, absences, presence and public holidays — the RD-ley 8/2019 working-time register. The whole API as Model Context Protocol tools, with OAuth 2.1 and API-key auth — connect Claude and other agents in seconds. ## Contract design [#contract-design] The API follows the patterns you would expect from a modern provider: * **Opaque identifiers** — the `id` key carries a UUID v7 string instead of an incremental integer. See [Pagination](/guides/pagination) for cursor semantics. * **Normalized errors** — every error returns an envelope with `type`, `code`, `message`, `param`, `doc_url` and `request_id`. See [Errors](/guides/errors). * **Idempotency keys** — supported on every `POST` to prevent duplicates on retries. See [Idempotency](/guides/idempotency). * **Rate limits per tier** — per-minute and monthly quotas, with `X-RateLimit-*` headers on every response. See [Rate limits](/guides/rate-limits). * **URL versioning** — `/v1/*`. Breaking changes trigger `/v2/*` with a documented deprecation policy. See [Versioning](/guides/versioning). * **Webhooks with dual-secret rotation** — HMAC SHA256, exponential retry with up to 8 attempts. See [Webhooks](/guides/webhooks). ## SDKs [#sdks] We ship official [TypeScript and PHP SDKs](/sdks) (`@factuarea/sdk` and `factuarea/factuarea-php`) with retries, idempotency, cursor pagination, typed errors and webhook verification built in. If your language isn't covered, any standard HTTP client (curl, Postman, axios, requests, Guzzle) works — the API is plain REST over JSON. The public REST API complements the Factuarea web client ([`app.factuarea.com`](https://app.factuarea.com)) — it doesn't replace it. Operations not exposed by the API (plan management, branding, global company tax configuration) still live in the app. --- # GET /v1/absence-balances — List all absence balances - **Operation ID**: `public-api.v1.absence-balances.list` - **Tag**: Absence Balances - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-balances/public-api.v1.absence-balances.list List your company’s absence balances with cursor-based pagination. Each balance is the accrued, carried-over and consumed days of one employee for one absence type in a given year, with the resulting `available_days`. Supports filtering by `employee_id` (UUID v7), `absence_type_id` (UUID v7) and `year`. Day amounts are exact decimal strings. ## Query parameters - `employee_id` (string | null, optional, format: uuid) — Employee ID (UUID v7) to filter balances by. - `absence_type_id` (string | null, optional, format: uuid) — Absence type ID (UUID v7) to filter balances by. - `year` (integer | null, optional, min 2000, max 2100) — Accrual year to filter balances by. - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the balance, a UUID v7. - `object` (string, required, enum: `absence_balance`) — Always `absence_balance`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the balance belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the absence type the balance tracks. - `absence_policy_id` (string, required, format: uuid) — UUID v7 of the absence policy the balance accrues under. - `year` (integer, required) — Accrual year the balance belongs to (e.g. `2026`). - `accrued_days` (string, required) — Days accrued so far this year, as an exact decimal string. - `carried_over_days` (string, required) — Days carried over from the previous year (capped by the policy), as an exact decimal string. - `carryover_expires_on` (string | null, required, format: date) — Date on which the carried-over days expire (`YYYY-MM-DD`), or `null` when they do not expire or there is no carryover. - `consumed_days` (string, required) — Days already consumed against this balance, as an exact decimal string. - `available_days` (string, required) — Usable balance (accrued + non-expired carried-over − consumed), as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the balance is measured: `days` or `hours`. - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-balances/{absence_balance} — Retrieve an absence balance - **Operation ID**: `public-api.v1.absence-balances.show` - **Tag**: Absence Balances - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-balances/public-api.v1.absence-balances.show Retrieve a single absence balance by its `id` (UUID v7), including its accrued, carried-over, consumed and available days for the employee, absence type and year. A balance belonging to another company returns 404 `absence_balance_not_found` (anti-enumeration). ## Path parameters - `absence_balance` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceBalance), required) — An absence balance for the Control Horario (time tracking) module: the accrued, carried-over, consumed and available days of one employee for one absence type in a given year. Day amounts are exact decimal strings. `available_days` is the usable balance (accrued + non-expired carried-over − consumed). - `id` (string, required, format: uuid) — Opaque identifier of the balance, a UUID v7. - `object` (string, required, enum: `absence_balance`) — Always `absence_balance`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the balance belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the absence type the balance tracks. - `absence_policy_id` (string, required, format: uuid) — UUID v7 of the absence policy the balance accrues under. - `year` (integer, required) — Accrual year the balance belongs to (e.g. `2026`). - `accrued_days` (string, required) — Days accrued so far this year, as an exact decimal string. - `carried_over_days` (string, required) — Days carried over from the previous year (capped by the policy), as an exact decimal string. - `carryover_expires_on` (string | null, required, format: date) — Date on which the carried-over days expire (`YYYY-MM-DD`), or `null` when they do not expire or there is no carryover. - `consumed_days` (string, required) — Days already consumed against this balance, as an exact decimal string. - `available_days` (string, required) — Usable balance (accrued + non-expired carried-over − consumed), as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the balance is measured: `days` or `hours`. - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-calendar — Get the team absence calendar - **Operation ID**: `public-api.v1.absence-calendar.show` - **Tag**: Absence Calendar - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-calendar/public-api.v1.absence-calendar.show Return the monthly absence calendar of your team for a given `year` and `month`: every active employee with their approved absences of that month (each coloured by its absence type) and the public holidays that apply, kept separate from the absences. Optionally scoped to a single `employee_id` (UUID v7). A computed resource: it exposes `employee_id` per member, never an `id`. ## Query parameters - `year` (integer, required, min 2000, max 2100) — Calendar year (4 digits). - `month` (integer, required, min 1, max 12) — Calendar month (1-12). - `employee_id` (string | null, optional, format: uuid) — Employee ID (UUID v7) to limit the calendar to a single employee (optional). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TeamAbsenceCalendar), required) — The monthly team absence calendar for the Control Horario (time tracking) module: for a given year and month, every active employee with their approved absences of that month (each coloured by its absence type) plus the public holidays that apply, kept separate from the absences. A computed resource with no entity identity: it is keyed by company + year/month and never exposes an `id`. - `year` (integer, required) — Year of the calendar. - `month` (integer, required) — Month of the calendar (1-12). - `members` (array, required) — Active employees of the team, each with their approved absences overlapping the month. - `holidays` (array, required) — Public holidays that apply to the team during the month. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-policies/{absence_policy}/archive — Archive an absence policy - **Operation ID**: `public-api.v1.absence-policies.archive` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.archive Archive an absence policy (transition `active` → `archived`), retiring it from use while preserving it. No request body. Returns 422 if it is already archived. Reversible via unarchive. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-policies/{absence_policy}/assign — Assign a policy to employees - **Operation ID**: `public-api.v1.absence-policies.assign` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.assign Assign the absence policy to one or more employees. `employee_ids` (a non-empty list of UUID v7, each belonging to your company) is required; an unknown employee returns 422. Returns the policy with its updated assigned-employee count. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `employee_ids`. - `employee_ids` (array, required) — Employee IDs (UUID v7) the policy is assigned to. ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-policies/{absence_policy}/assignments — List a policy’s assigned employees - **Operation ID**: `public-api.v1.absence-policies.assignments` - **Tag**: Absence Policies - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.assignments List the employees assigned to this absence policy (their `employee_id` UUID v7 and name), as a flat list under `{ "data": [ … ] }`. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `absence_policy_assignment`) — Always `absence_policy_assignment`. - `employee_id` (string, required, format: uuid) — UUID v7 of the assigned employee. - `employee_name` (string, required) — Full name of the assigned employee. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-policies/{absence_policy}/carryover — Configure a policy’s year-end carryover - **Operation ID**: `public-api.v1.absence-policies.carryover` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.carryover Configure how much unused balance carries over at year-end for this absence policy. `carryover_type` (`none`/`capped`/`unlimited`) is required; `carryover_max_days` is required and positive only when `carryover_type` is `capped`. Optional `carryover_expiry_month` (1..12) and `carryover_expiry_day` set when the carried-over balance expires. A policy belonging to another company returns 404 `absence_policy_not_found` (anti-enumeration). Returns the updated policy. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 4 properties; 1 required: `carryover_type`. - `carryover_type` (string, required, enum: `none`, `capped`, `unlimited`) — Carryover mode: `none` (no carryover), `capped` (with a cap) or `unlimited` (no cap). - `carryover_max_days` (integer | null, optional, min 1) — Cap of carryover days; required and positive when the mode is `capped`. - `carryover_expiry_month` (integer | null, optional, min 1, max 12) — Carryover expiry month (1-12); when provided, the day is also required. - `carryover_expiry_day` (integer | null, optional, min 1, max 31) — Carryover expiry day (1-31); when provided, the month is also required. ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-policies — Create an absence policy - **Operation ID**: `public-api.v1.absence-policies.create` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.create Create an absence policy for the authenticated company (resolved from the API key, never from the payload). `name`, `allowance_type` (`limited`/`unlimited`) and `accrual_method` (`annual`/`monthly`) are required; `allowance_days` is required and positive only when `allowance_type` is `limited`. `absence_type_ids` is the list of absence type UUIDs (v7) the policy covers (may be empty); a type belonging to another company returns 422. Returns the created policy with its generated `id` (UUID v7). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 4 required: `name`, `allowance_type`, `accrual_method`, `absence_type_ids`. - `name` (string, required, maxLength 120) — Absence policy name. - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Day allowance type: `limited` or `unlimited`. - `allowance_days` (integer | null, optional, min 1) — Days allotted per year (required and positive only when the allowance is `limited`). - `accrual_method` (string, required, enum: `annual`, `monthly`) — Day accrual method: `annual` or `monthly`. - `absence_type_ids` (array, required) — Absence type IDs (UUID v7) associated with the policy (may be empty). ## Responses - **201** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-policies — List all absence policies - **Operation ID**: `public-api.v1.absence-policies.list` - **Tag**: Absence Policies - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.list List your company’s absence policies with cursor-based pagination. Supports filtering by `status` (`active`/`archived`) and `accrual_method` (`annual`/`monthly`), plus free-text `search` over the policy name. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional, enum: `active`, `archived`) — Lifecycle status of the absence policy. - `status[in]` (string, optional) — Lifecycle status of the absence policy. - `accrual_method` (string, optional, enum: `annual`, `monthly`) — Accrual method used to grant the absence balance. - `accrual_method[in]` (string, optional) — Accrual method used to grant the absence balance. - `search` (string, optional, maxLength 80) — Free-text search. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-policies/{absence_policy} — Retrieve an absence policy - **Operation ID**: `public-api.v1.absence-policies.show` - **Tag**: Absence Policies - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.show Retrieve a single absence policy by its `id` (UUID v7), including the UUIDs of its associated absence types and the count of assigned employees. A policy belonging to another company returns 404 `absence_policy_not_found` (anti-enumeration). ## Path parameters - `absence_policy` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-policies/{absence_policy}/unarchive — Unarchive an absence policy - **Operation ID**: `public-api.v1.absence-policies.unarchive` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.unarchive Unarchive an absence policy (transition `archived` → `active`), returning it to use. No request body. Returns 422 if it is already active. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-policies/{absence_policy}/unassign — Unassign a policy from employees - **Operation ID**: `public-api.v1.absence-policies.unassign` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.unassign Remove the assignment of the absence policy from one or more employees. `employee_ids` (a non-empty list of UUID v7) is required; removing an assignment that does not exist is a no-op. Returns the policy with its updated assigned-employee count. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `employee_ids`. - `employee_ids` (array, required) — Employee IDs (UUID v7) whose policy assignment is removed. ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/absence-policies/{absence_policy} — Update an absence policy - **Operation ID**: `public-api.v1.absence-policies.update` - **Tag**: Absence Policies - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-policies/public-api.v1.absence-policies.update Partially update an absence policy: only the fields present in the payload are changed; omitted fields keep their current value. When `absence_type_ids` is provided it fully replaces the associated types. Returns the updated policy. ## Path parameters - `absence_policy` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 5 properties; none of them required. - `name` (string | null, optional, maxLength 120) — Absence policy name. - `allowance_type` (string | null, optional, enum: `limited`, `unlimited`) — Day allowance type: `limited` or `unlimited`. - `allowance_days` (integer | null, optional, min 1) — Days allotted per year (positive; the domain requires a value when the allowance is `limited`). - `accrual_method` (string | null, optional, enum: `annual`, `monthly`) — Day accrual method: `annual` or `monthly`. - `absence_type_ids` (array, optional) — Absence type IDs (UUID v7) associated (when provided, replaces the association). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsencePolicy), required) — An absence policy for the Control Horario (time tracking) module: how many days per year an employee accrues for a set of absence types, how those days accrue, and which employees it applies to. `allowance_type` decides whether the yearly allowance is capped (`limited`, with `allowance_days`) or `unlimited`. - `id` (string, required, format: uuid) — Opaque identifier of the policy, a UUID v7. - `object` (string, required, enum: `absence_policy`) — Always `absence_policy`. - `name` (string, required) — Human-readable name of the policy (e.g. `Vacaciones estándar`). - `allowance_type` (string, required, enum: `limited`, `unlimited`) — Whether the yearly allowance is capped (`limited`) or `unlimited`. - `allowance_days` (integer | null, required) — Days accrued per year when `allowance_type` is `limited`; `null` when `unlimited`. - `accrual_method` (string, required, enum: `annual`, `monthly`) — How the allowance accrues: `annual` (all at once) or `monthly` (prorated). - `absence_type_ids` (array, required) — UUID v7 of each absence type this policy covers. - `assigned_employee_count` (integer, required) — Number of employees currently assigned to the policy. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `carryover` (object, required) — How much unused balance carries over at year-end under this policy. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-requests/{absence_request}/approve — Approve an absence request - **Operation ID**: `public-api.v1.absence-requests.approve` - **Tag**: Absence Requests - **Required scope**: `absences:transition` — Change the lifecycle status of absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-requests/public-api.v1.absence-requests.approve Approve a pending absence request (transition `pending` → `approved`), consuming the employee’s balance. No request body (an optional `note` is accepted). A reviewer cannot approve the request they themselves created (422). Returns the updated request. ## Path parameters - `absence_request` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. - `note` (string | null, optional, maxLength 1000) — Approval note (optional). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceRequest), required) — An absence request for the Control Horario (time tracking) module: an employee’s request to be off for a date range under an absence type. `status` walks `pending → approved | rejected | cancelled`. `day_amount` is the requested amount as an exact decimal string, measured in `measurement_unit` (days or hours). Review fields (`reviewer_user_id`, `review_note`, `reviewed_at`) are `null` while the request is still pending. - `id` (string, required, format: uuid) — Opaque identifier of the absence request, a UUID v7. - `object` (string, required, enum: `absence_request`) — Always `absence_request`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the request belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the requested absence type. - `absence_type_name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `color` (string, required) — Hex color `#RRGGBB` of the absence type, used to render the request on the calendar. - `status` (string, required, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status of the request: `pending`, `approved`, `rejected` or `cancelled`. - `start_date` (string, required, format: date) — First day of the absence (`YYYY-MM-DD`). - `end_date` (string, required, format: date) — Last day of the absence (`YYYY-MM-DD`). - `day_amount` (string, required) — Requested amount (working days or hours) as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit the request is measured in: `days` or `hours`. - `note` (string | null, required) — Optional note the employee attached when requesting. - `reviewer_user_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while pending. - `review_note` (string | null, required) — Reason given on rejection (or note on approval); `null` while pending. - `reviewed_at` (string | null, required, format: date-time) — Timestamp of the approval/rejection (ISO 8601); `null` while pending. - `created_at` (string, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-requests/{absence_request}/cancel — Cancel an absence request - **Operation ID**: `public-api.v1.absence-requests.cancel` - **Tag**: Absence Requests - **Required scope**: `absences:transition` — Change the lifecycle status of absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-requests/public-api.v1.absence-requests.cancel Cancel an absence request. If it was approved, the consumed balance is released back. No request body. A request belonging to another company returns 404 `absence_request_not_found` (anti-enumeration). Returns the updated request. ## Path parameters - `absence_request` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceRequest), required) — An absence request for the Control Horario (time tracking) module: an employee’s request to be off for a date range under an absence type. `status` walks `pending → approved | rejected | cancelled`. `day_amount` is the requested amount as an exact decimal string, measured in `measurement_unit` (days or hours). Review fields (`reviewer_user_id`, `review_note`, `reviewed_at`) are `null` while the request is still pending. - `id` (string, required, format: uuid) — Opaque identifier of the absence request, a UUID v7. - `object` (string, required, enum: `absence_request`) — Always `absence_request`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the request belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the requested absence type. - `absence_type_name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `color` (string, required) — Hex color `#RRGGBB` of the absence type, used to render the request on the calendar. - `status` (string, required, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status of the request: `pending`, `approved`, `rejected` or `cancelled`. - `start_date` (string, required, format: date) — First day of the absence (`YYYY-MM-DD`). - `end_date` (string, required, format: date) — Last day of the absence (`YYYY-MM-DD`). - `day_amount` (string, required) — Requested amount (working days or hours) as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit the request is measured in: `days` or `hours`. - `note` (string | null, required) — Optional note the employee attached when requesting. - `reviewer_user_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while pending. - `review_note` (string | null, required) — Reason given on rejection (or note on approval); `null` while pending. - `reviewed_at` (string | null, required, format: date-time) — Timestamp of the approval/rejection (ISO 8601); `null` while pending. - `created_at` (string, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-requests — Create an absence request - **Operation ID**: `public-api.v1.absence-requests.create` - **Tag**: Absence Requests - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-requests/public-api.v1.absence-requests.create Create an absence request for the authenticated company (resolved from the API key, never from the payload). `employee_id` (UUID v7) is required — an API key acts as a system, so the target employee must be given. `absence_type_id` (UUID v7) and the `start_date`/`end_date` range (`YYYY-MM-DD`, end on or after start) are required; `note` is optional. The requested amount is computed in working days minus the applicable public holidays. If the absence type does not require approval it is auto-approved and consumes the balance. Returns the created request with its generated `id` (UUID v7). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 4 required: `employee_id`, `absence_type_id`, `start_date`, `end_date`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) requesting the absence. - `absence_type_id` (string, required, format: uuid) — Requested absence type ID (UUID v7). - `start_date` (string, required, format: date) — Absence start date, in `Y-m-d`. - `end_date` (string, required, format: date) — Absence end date, in `Y-m-d`, equal to or after the start. - `note` (string | null, optional, maxLength 1000) — Optional note for the request. ## Responses - **201** - Body (`application/json`): - `data` (object (AbsenceRequest), required) — An absence request for the Control Horario (time tracking) module: an employee’s request to be off for a date range under an absence type. `status` walks `pending → approved | rejected | cancelled`. `day_amount` is the requested amount as an exact decimal string, measured in `measurement_unit` (days or hours). Review fields (`reviewer_user_id`, `review_note`, `reviewed_at`) are `null` while the request is still pending. - `id` (string, required, format: uuid) — Opaque identifier of the absence request, a UUID v7. - `object` (string, required, enum: `absence_request`) — Always `absence_request`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the request belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the requested absence type. - `absence_type_name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `color` (string, required) — Hex color `#RRGGBB` of the absence type, used to render the request on the calendar. - `status` (string, required, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status of the request: `pending`, `approved`, `rejected` or `cancelled`. - `start_date` (string, required, format: date) — First day of the absence (`YYYY-MM-DD`). - `end_date` (string, required, format: date) — Last day of the absence (`YYYY-MM-DD`). - `day_amount` (string, required) — Requested amount (working days or hours) as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit the request is measured in: `days` or `hours`. - `note` (string | null, required) — Optional note the employee attached when requesting. - `reviewer_user_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while pending. - `review_note` (string | null, required) — Reason given on rejection (or note on approval); `null` while pending. - `reviewed_at` (string | null, required, format: date-time) — Timestamp of the approval/rejection (ISO 8601); `null` while pending. - `created_at` (string, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-requests — List all absence requests - **Operation ID**: `public-api.v1.absence-requests.list` - **Tag**: Absence Requests - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-requests/public-api.v1.absence-requests.list List your company’s absence requests with cursor-based pagination. Supports filtering by `employee_id` (UUID v7), `absence_type_id` (UUID v7), `status` (`pending`/`approved`/`rejected`/`cancelled`) and by date range (`from`/`to`, `YYYY-MM-DD`). ## Query parameters - `employee_id` (string | null, optional, format: uuid) — Employee ID (UUID v7) to filter requests by. - `absence_type_id` (string | null, optional, format: uuid) — Absence type ID (UUID v7) to filter requests by. - `status` (string | null, optional, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status to filter by (pending/approved/rejected/cancelled). - `from` (string | null, optional, format: date) — Start date (Y-m-d) to filter the request range by. - `to` (string | null, optional, format: date) — End date (Y-m-d) to filter the request range by. - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the absence request, a UUID v7. - `object` (string, required, enum: `absence_request`) — Always `absence_request`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the request belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the requested absence type. - `absence_type_name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `color` (string, required) — Hex color `#RRGGBB` of the absence type, used to render the request on the calendar. - `status` (string, required, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status of the request: `pending`, `approved`, `rejected` or `cancelled`. - `start_date` (string, required, format: date) — First day of the absence (`YYYY-MM-DD`). - `end_date` (string, required, format: date) — Last day of the absence (`YYYY-MM-DD`). - `day_amount` (string, required) — Requested amount (working days or hours) as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit the request is measured in: `days` or `hours`. - `note` (string | null, required) — Optional note the employee attached when requesting. - `reviewer_user_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while pending. - `review_note` (string | null, required) — Reason given on rejection (or note on approval); `null` while pending. - `reviewed_at` (string | null, required, format: date-time) — Timestamp of the approval/rejection (ISO 8601); `null` while pending. - `created_at` (string, required, format: date-time) — Creation timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-requests/{absence_request}/reject — Reject an absence request - **Operation ID**: `public-api.v1.absence-requests.reject` - **Tag**: Absence Requests - **Required scope**: `absences:transition` — Change the lifecycle status of absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-requests/public-api.v1.absence-requests.reject Reject a pending absence request (transition `pending` → `rejected`). A `reason` is required (422 without it); rejecting neither consumes nor releases balance. Returns the updated request. ## Path parameters - `absence_request` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `reason`. - `reason` (string, required, maxLength 1000) — Rejection reason (required). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceRequest), required) — An absence request for the Control Horario (time tracking) module: an employee’s request to be off for a date range under an absence type. `status` walks `pending → approved | rejected | cancelled`. `day_amount` is the requested amount as an exact decimal string, measured in `measurement_unit` (days or hours). Review fields (`reviewer_user_id`, `review_note`, `reviewed_at`) are `null` while the request is still pending. - `id` (string, required, format: uuid) — Opaque identifier of the absence request, a UUID v7. - `object` (string, required, enum: `absence_request`) — Always `absence_request`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the request belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the requested absence type. - `absence_type_name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `color` (string, required) — Hex color `#RRGGBB` of the absence type, used to render the request on the calendar. - `status` (string, required, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status of the request: `pending`, `approved`, `rejected` or `cancelled`. - `start_date` (string, required, format: date) — First day of the absence (`YYYY-MM-DD`). - `end_date` (string, required, format: date) — Last day of the absence (`YYYY-MM-DD`). - `day_amount` (string, required) — Requested amount (working days or hours) as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit the request is measured in: `days` or `hours`. - `note` (string | null, required) — Optional note the employee attached when requesting. - `reviewer_user_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while pending. - `review_note` (string | null, required) — Reason given on rejection (or note on approval); `null` while pending. - `reviewed_at` (string | null, required, format: date-time) — Timestamp of the approval/rejection (ISO 8601); `null` while pending. - `created_at` (string, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-requests/{absence_request} — Retrieve an absence request - **Operation ID**: `public-api.v1.absence-requests.show` - **Tag**: Absence Requests - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-requests/public-api.v1.absence-requests.show Retrieve a single absence request by its `id` (UUID v7), including its type, date range, requested amount, lifecycle status and review fields. A request belonging to another company returns 404 `absence_request_not_found` (anti-enumeration). ## Path parameters - `absence_request` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceRequest), required) — An absence request for the Control Horario (time tracking) module: an employee’s request to be off for a date range under an absence type. `status` walks `pending → approved | rejected | cancelled`. `day_amount` is the requested amount as an exact decimal string, measured in `measurement_unit` (days or hours). Review fields (`reviewer_user_id`, `review_note`, `reviewed_at`) are `null` while the request is still pending. - `id` (string, required, format: uuid) — Opaque identifier of the absence request, a UUID v7. - `object` (string, required, enum: `absence_request`) — Always `absence_request`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the request belongs to. - `absence_type_id` (string, required, format: uuid) — UUID v7 of the requested absence type. - `absence_type_name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `color` (string, required) — Hex color `#RRGGBB` of the absence type, used to render the request on the calendar. - `status` (string, required, enum: `pending`, `approved`, `rejected`, `cancelled`) — Lifecycle status of the request: `pending`, `approved`, `rejected` or `cancelled`. - `start_date` (string, required, format: date) — First day of the absence (`YYYY-MM-DD`). - `end_date` (string, required, format: date) — Last day of the absence (`YYYY-MM-DD`). - `day_amount` (string, required) — Requested amount (working days or hours) as an exact decimal string. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit the request is measured in: `days` or `hours`. - `note` (string | null, required) — Optional note the employee attached when requesting. - `reviewer_user_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while pending. - `review_note` (string | null, required) — Reason given on rejection (or note on approval); `null` while pending. - `reviewed_at` (string | null, required, format: date-time) — Timestamp of the approval/rejection (ISO 8601); `null` while pending. - `created_at` (string, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-types/{absence_type}/archive — Archive an absence type - **Operation ID**: `public-api.v1.absence-types.archive` - **Tag**: Absence Types - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-types/public-api.v1.absence-types.archive Archive an absence type (transition `active` → `archived`), retiring it from use while preserving it. No request body. Returns 422 if it is already archived. Reversible via unarchive. ## Path parameters - `absence_type` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceType), required) — A configurable absence type for the Control Horario (time tracking) module — what an employee can request (holidays, sick leave, paid leave, …). Its flags decide whether the period is paid (`is_paid`) and whether it needs manager approval (`requires_approval`); `measurement_unit` fixes whether balances are tracked in days or hours. - `id` (string, required, format: uuid) — Opaque identifier of the absence type, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `absence_type`) — Always `absence_type`. - `name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager’s approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required) — Hex color `#RRGGBB` used to render the type on the calendar. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. - `is_system` (boolean, required) — Whether it is a default system type seeded for every company. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-types — Create an absence type - **Operation ID**: `public-api.v1.absence-types.create` - **Tag**: Absence Types - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-types/public-api.v1.absence-types.create Create an absence type for the authenticated company (resolved from the API key, never from the payload). `name`, `is_paid`, `requires_approval`, `measurement_unit` (`days`/`hours`), `color` (hex `#RRGGBB`) and `visibility` (`everyone`/`managers_only`) are all required. Returns the created type with its generated `id` (UUID v7). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 6 required: `name`, `is_paid`, `requires_approval`, `measurement_unit`, `color`, `visibility`. - `name` (string, required, maxLength 120) — Absence type name. - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager's approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required, pattern: `^#[0-9A-Fa-f]{6}$`) — Type color for the calendar, in `#RRGGBB` hexadecimal format. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. ## Responses - **201** - Body (`application/json`): - `data` (object (AbsenceType), required) — A configurable absence type for the Control Horario (time tracking) module — what an employee can request (holidays, sick leave, paid leave, …). Its flags decide whether the period is paid (`is_paid`) and whether it needs manager approval (`requires_approval`); `measurement_unit` fixes whether balances are tracked in days or hours. - `id` (string, required, format: uuid) — Opaque identifier of the absence type, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `absence_type`) — Always `absence_type`. - `name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager’s approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required) — Hex color `#RRGGBB` used to render the type on the calendar. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. - `is_system` (boolean, required) — Whether it is a default system type seeded for every company. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-types — List all absence types - **Operation ID**: `public-api.v1.absence-types.list` - **Tag**: Absence Types - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-types/public-api.v1.absence-types.list List your company’s absence types with cursor-based pagination. Supports filtering by `status` (`active`/`archived`) and `measurement_unit` (`days`/`hours`), plus free-text `search` over the type name. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional, enum: `active`, `archived`) — Lifecycle status of the absence type. - `status[in]` (string, optional) — Lifecycle status of the absence type. - `measurement_unit` (string, optional, enum: `days`, `hours`) — Measurement unit used to quantify the absence type. - `measurement_unit[in]` (string, optional) — Measurement unit used to quantify the absence type. - `search` (string, optional, maxLength 80) — Free-text search. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the absence type, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `absence_type`) — Always `absence_type`. - `name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager’s approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required) — Hex color `#RRGGBB` used to render the type on the calendar. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. - `is_system` (boolean, required) — Whether it is a default system type seeded for every company. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/absence-types/{absence_type} — Retrieve an absence type - **Operation ID**: `public-api.v1.absence-types.show` - **Tag**: Absence Types - **Required scope**: `absences:read` — Read absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-types/public-api.v1.absence-types.show Retrieve a single absence type by its `id` (UUID v7). A type belonging to another company returns 404 `absence_type_not_found` (anti-enumeration). ## Path parameters - `absence_type` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceType), required) — A configurable absence type for the Control Horario (time tracking) module — what an employee can request (holidays, sick leave, paid leave, …). Its flags decide whether the period is paid (`is_paid`) and whether it needs manager approval (`requires_approval`); `measurement_unit` fixes whether balances are tracked in days or hours. - `id` (string, required, format: uuid) — Opaque identifier of the absence type, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `absence_type`) — Always `absence_type`. - `name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager’s approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required) — Hex color `#RRGGBB` used to render the type on the calendar. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. - `is_system` (boolean, required) — Whether it is a default system type seeded for every company. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/absence-types/{absence_type}/unarchive — Unarchive an absence type - **Operation ID**: `public-api.v1.absence-types.unarchive` - **Tag**: Absence Types - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-types/public-api.v1.absence-types.unarchive Unarchive an absence type (transition `archived` → `active`), returning it to use. No request body. Returns 422 if it is already active. ## Path parameters - `absence_type` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceType), required) — A configurable absence type for the Control Horario (time tracking) module — what an employee can request (holidays, sick leave, paid leave, …). Its flags decide whether the period is paid (`is_paid`) and whether it needs manager approval (`requires_approval`); `measurement_unit` fixes whether balances are tracked in days or hours. - `id` (string, required, format: uuid) — Opaque identifier of the absence type, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `absence_type`) — Always `absence_type`. - `name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager’s approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required) — Hex color `#RRGGBB` used to render the type on the calendar. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. - `is_system` (boolean, required) — Whether it is a default system type seeded for every company. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/absence-types/{absence_type} — Update an absence type - **Operation ID**: `public-api.v1.absence-types.update` - **Tag**: Absence Types - **Required scope**: `absences:write` — Create and update absences. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/absence-types/public-api.v1.absence-types.update Partially update an absence type: only the fields present in the payload are changed; omitted fields keep their current value. Returns the updated type. ## Path parameters - `absence_type` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 6 properties; none of them required. - `name` (string | null, optional, maxLength 120) — Absence type name. - `is_paid` (boolean | null, optional) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean | null, optional) — Whether requesting this absence requires a manager's approval. - `measurement_unit` (string | null, optional, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string | null, optional, pattern: `^#[0-9A-Fa-f]{6}$`) — Type color for the calendar, in `#RRGGBB` hexadecimal format. - `visibility` (string | null, optional, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. ## Responses - **200** - Body (`application/json`): - `data` (object (AbsenceType), required) — A configurable absence type for the Control Horario (time tracking) module — what an employee can request (holidays, sick leave, paid leave, …). Its flags decide whether the period is paid (`is_paid`) and whether it needs manager approval (`requires_approval`); `measurement_unit` fixes whether balances are tracked in days or hours. - `id` (string, required, format: uuid) — Opaque identifier of the absence type, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `absence_type`) — Always `absence_type`. - `name` (string, required) — Human-readable name of the absence type (e.g. `Vacaciones`). - `is_paid` (boolean, required) — Whether the absence is paid (the employee is compensated for the period). - `requires_approval` (boolean, required) — Whether requesting this absence requires a manager’s approval. - `measurement_unit` (string, required, enum: `days`, `hours`) — Unit in which the absence is measured: `days` or `hours`. - `color` (string, required) — Hex color `#RRGGBB` used to render the type on the calendar. - `visibility` (string, required, enum: `everyone`, `managers_only`) — Who can see the type: `everyone` or `managers_only`. - `is_system` (boolean, required) — Whether it is a default system type seeded for every company. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (usable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/account/api-keys — Create an API key - **Operation ID**: `public-api.v1.account.api_keys.create` - **Tag**: Account - **Required scope**: `account:write` — Create and update account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.api_keys.create Create a new API key and return its plaintext `secret` exactly once — store it now, it cannot be retrieved later. Requesting a scope above the holder's plan or outside the catalog returns 422. Pass `environment: test` to mint a sandbox key (`fact_test_`) with no real-world side effects; omit it for a live key (`fact_live_`). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 2 required: `name`, `scopes`. Create an API key for your own tenant. The `tier` is never accepted from the body — it is derived from the holder plan and add-ons. Requested `scopes` must belong to the closed v1 catalog and stay within the holder plan; a scope above the plan returns 422. - `name` (string, required, maxLength 120, minLength 1) — Human-readable name for the API key (1-120 characters). - `expires_at` (string | null, optional, format: date-time) — Future ISO 8601 date after which the key stops authenticating. - `environment` (string | null, optional, enum: `live`, `test`) — Key environment: `live` (production) or `test` (sandbox). Defaults to `live`. - `scopes` (array, required) — List of scopes from the closed v1 catalog (at least one). - `ip_allowlist` (array | null, optional) — Optional list of allowed IPs / CIDR ranges (IPv4, IPv6, /N). ## Responses - **201** - Body (`application/json`): - `data` (object (ApiKeyWithSecret), required) — An API key returned once at creation or after secret rotation. Includes the plaintext `secret` — store it now, it cannot be retrieved later. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - `secret` (string, required) — Plaintext secret. Returned only at creation or after rotation — never again. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed — e.g. the API key plan limit was reached, or an invoice language outside the allowed catalog (`es`, `en`, `ca`). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/account/api-keys — List your API keys - **Operation ID**: `public-api.v1.account.api_keys.list` - **Tag**: Account - **Required scope**: `account:read` — Read account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.api_keys.list List the API keys of the authenticated company with cursor-based pagination. Each key exposes its `prefix`, `scopes`, `tier`, `environment` (`live`/`test`) and lifecycle timestamps. The plaintext secret is never returned — it is shown once, at creation or rotation. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `ApiKeyListV1Resource` - Body (`application/json`): - `data` (array, required) — Page of API keys. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - `has_more` (boolean, required) — `true` when more keys exist beyond this page. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/account/api-keys/{api_key}/revoke — Revoke an API key - **Operation ID**: `public-api.v1.account.api_keys.revoke` - **Tag**: Account - **Required scope**: `account:write` — Create and update account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.api_keys.revoke Revoke an API key immediately and irreversibly. Subsequent requests authenticated with that key fail with 401. You may revoke the key currently in use — doing so cuts off your own access. Revoking a key of another company returns 404 `api_key_not_found`. ## Path parameters - `api_key` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. Optionally record why the API key is being revoked. `reason` (string, ≤500 chars) is optional; when omitted a default reason is stored for auditing. - `reason` (string | null, optional, maxLength 500) — Optional reason for the revocation (recorded in the audit log). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiKey), required) — An API key of your company. The plaintext secret is never exposed in this representation — it is shown only once, at creation or after rotation. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed — e.g. the API key plan limit was reached, or an invoice language outside the allowed catalog (`es`, `en`, `ca`). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/account/api-keys/{api_key}/rotate_secret — Rotate an API key secret - **Operation ID**: `public-api.v1.account.api_keys.rotate_secret` - **Tag**: Account - **Required scope**: `account:write` — Create and update account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.api_keys.rotate_secret Invalidate the current secret of an API key immediately, generate a fresh `prefix` + `secret`, and return the new secret in plaintext exactly once. Any request made with the previous secret stops authenticating right away. Irreversible. ## Path parameters - `api_key` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiKeyWithSecret), required) — An API key returned once at creation or after secret rotation. Includes the plaintext `secret` — store it now, it cannot be retrieved later. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - `secret` (string, required) — Plaintext secret. Returned only at creation or after rotation — never again. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed — e.g. the API key plan limit was reached, or an invoice language outside the allowed catalog (`es`, `en`, `ca`). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/account/api-keys/{api_key} — Retrieve an API key - **Operation ID**: `public-api.v1.account.api_keys.show` - **Tag**: Account - **Required scope**: `account:read` — Read account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.api_keys.show Retrieve a single API key of the authenticated company by its `id` (UUID v7). The plaintext secret is never included. A key belonging to another company returns 404 `api_key_not_found` (anti-enumeration). ## Path parameters - `api_key` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiKey), required) — An API key of your company. The plaintext secret is never exposed in this representation — it is shown only once, at creation or after rotation. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/account/billing — Retrieve account billing details - **Operation ID**: `public-api.v1.account.billing` - **Tag**: Account - **Required scope**: `account:read` — Read account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.billing Returns the subscription billing snapshot of the authenticated company: base plan subscription (status, trial, current period end, pending plan change), gestoría seats subscription (quantity, active managed companies, per-seat cost with VAT, recurring total, next invoice) and default payment method. Managed companies (plan `gestionada`) receive `managed: true` without the master's billing data. Amounts are integer cents; unresolved amounts are `null`, never a misleading 0. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AccountBilling), required) — Billing snapshot of the effective tenant: current plan (trial, grace period, pending change), gestoría seat subscription (when the account manages companies) and payment method. Managed child companies return `managed: true` with the synthetic `gestionada` plan — the master's billing never leaks to a child key. - `object` (string, required, enum: `account_billing`) — Stripe-like discriminator. Always `account_billing` for this resource. - `managed` (boolean, required) — true when the effective tenant is a managed child company (gestoría): its billing is the master's seat, so `gestoria_seats` and `payment_method` are null. - `plan` (object, required) — Current plan subscription of the account. - `gestoria_seats` (object | null, required) — Gestoría seat subscription of the account, or null when the tenant does not manage companies. Amounts are in minor currency units (cents) and are null when the recurring cost is not resolvable (enterprise outside Stripe, sandbox, no active plan) — never a misleading 0. - `payment_method` (object | null, required) — Default payment method on file, or null when none is configured. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/account/personalization/templates — List available personalization templates - **Operation ID**: `public-api.v1.account.personalization.templates` - **Tag**: Account - **Required scope**: `account:read` — Read account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.personalization.templates List the PDF templates available for the account's plan (plan-aware) plus the accepted format for the `accent_color`. Use it to discover which `pdf_template` slugs and colors can be set via `PATCH /v1/account/personalization`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AccountPersonalizationTemplates), required) — Plan-aware catalog of PDF templates available for the account, plus the accepted hex format for the accent color. Returned by `GET /v1/account/personalization/templates`. Use it to discover which `pdf_template` slugs and colors can be set via `PATCH /v1/account/personalization`. - `object` (string, required, enum: `personalization_templates`) — Stripe-like discriminator. Always `personalization_templates` for this resource. - `templates` (array, required) — Available PDF templates for the account's plan. - `accent_color` (object, required) — Accepted format for the `accent_color` of the PDF. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PATCH /v1/account/personalization — Update account personalization - **Operation ID**: `public-api.v1.account.personalization.update` - **Tag**: Account - **Required scope**: `account:write` — Create and update account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.personalization.update Set the invoice-emission language, PDF template and accent color of the company in one partial update; omitted fields keep their value. `language` is one of `es`, `en`, `ca`; `pdf_template` is a slug from the `PdfTemplate` catalog; `accent_color` is a `#RRGGBB` hex color. Returns the updated `Account` resource. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 3 properties; none of them required. Partial update of the account personalization; only the fields present in the body are applied. `language` is one of `es`, `en`, `ca`; `pdf_template` is a template slug; `accent_color` is a `#RRGGBB` hex color. Values outside the catalog return 422 with the accepted values. - `language` (string | null, optional, enum: `es`, `en`, `ca`) — Account issuing language (es, en or ca). - `pdf_template` (string | null, optional, enum: `classic`, `modern`, `minimal`, `corporative`, `premium`) — Invoice PDF template (slug from the template catalog). - `accent_color` (string | null, optional, pattern: `^#[0-9A-Fa-f]{6}$`) — PDF accent color in hexadecimal (#RRGGBB). ## Responses - **200** - Body (`application/json`): - `data` (object (Account), required) — Snapshot of the company, plan, developer addon status and metadata of the API key used to make the request. Use this endpoint to introspect credentials and discover limits with a single call. - `object` (string, required, enum: `account`) — Stripe-like discriminator. Always `account` for this resource. - `company` (object, required) - `plan` (object, required) - `addon` (object, required) — State of the `developer_api` addon for this company. - `api_key` (object, required) — Metadata of the API key used to authenticate the request. The secret is never returned (it is only shown once at creation time). - `personalization` (object, required) — Account personalization: invoice-emission language and PDF template/accent color. Mutable via `PATCH /v1/account/personalization`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed — e.g. the API key plan limit was reached, or an invoice language outside the allowed catalog (`es`, `en`, `ca`). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/account — Retrieve account details - **Operation ID**: `public-api.v1.account.show` - **Tag**: Account - **Required scope**: `account:read` — Read account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.show Stripe-like account endpoint: returns the authenticated company together with its plan, add-ons, and the metadata of the API key in use (environment, scopes). Use it to introspect what the current key can do. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Account), required) — Snapshot of the company, plan, developer addon status and metadata of the API key used to make the request. Use this endpoint to introspect credentials and discover limits with a single call. - `object` (string, required, enum: `account`) — Stripe-like discriminator. Always `account` for this resource. - `company` (object, required) - `plan` (object, required) - `addon` (object, required) — State of the `developer_api` addon for this company. - `api_key` (object, required) — Metadata of the API key used to authenticate the request. The secret is never returned (it is only shown once at creation time). - `personalization` (object, required) — Account personalization: invoice-emission language and PDF template/accent color. Mutable via `PATCH /v1/account/personalization`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/account/census-verification — Verify account against the AEAT census - **Operation ID**: `public-api.v1.account.verify_census` - **Tag**: Account - **Required scope**: `account:read` — Read account. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/account/public-api.v1.account.verify_census Check the persisted company name + tax ID pair against the AEAT census (VNifV2) to anticipate VeriFactu 4104 rejections. No request body: the endpoint always verifies the account's persisted fiscal data. Fail-open — if AEAT is unreachable the call returns 200 with `status: unavailable`. Test keys (`fact_test_`) return deterministic statuses per magic NIF without contacting AEAT. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (CensusVerification), required) — Result of verifying the account's persisted company name + tax ID pair against the AEAT census (VNifV2). Use it to anticipate VeriFactu 4104 rejections before invoicing. Fail-open: returns `unavailable` when AEAT cannot be reached. - `object` (string, required, enum: `census_verification`) — Stripe-like discriminator. Always `census_verification` for this resource. - `status` (string, required, enum: `identified`, `not_identified`, `not_identified_similar`, `identified_inactive`, `identified_revoked`, `unavailable`) — Census result. `identified`: name + tax ID match an active taxpayer. `not_identified`: the pair is not in the census. `not_identified_similar`: a similar individual exists (natural persons only). `identified_inactive` / `identified_revoked`: the taxpayer is deregistered or revoked. `unavailable`: AEAT could not answer (timeout, fault, no platform certificate) — verification is informational and never blocks. - `verified_name` (string | null, required) — The company name that was checked against the census (the persisted account name). `null` when the account has never been verified. - `checked_at` (string | null, required, format: date-time) — ISO 8601 timestamp of the last verification. `null` when never verified. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/clients/{client}/activities — List client activity timeline - **Operation ID**: `public-api.v1.clients.activities` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.activities Return the audit timeline for a client combining its own domain events plus invoice, quote, delivery note, proforma and purchase invoice events that reference it. Paginated with page and per_page query params (default 50). ## Path parameters - `client` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `client.updated`, `invoice.created`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients/bulk-create — Bulk create clients - **Operation ID**: `public-api.v1.clients.bulk_create` - **Tag**: Clients - **Required scope**: `clients:write` — Create and update clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.bulk_create Create up to 500 clients in one call, each entry a full client payload. With `dry_run=true` it validates every row without persisting and returns a per-row classification (`results[]`, including duplicate `external_id`/`tax_id` and a non-blocking AEAT census warning); with `dry_run=false` it creates only the valid rows and reports the rest in `failures[]`. Returns the `BulkCreateResult` shape. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `clients`. Create clients in bulk. `clients[]` holds up to 500 client payloads and `dry_run` (default `false`) validates each row without persisting. Per-row rules (format, duplicate `external_id`/`tax_id`, AEAT census) are reported per row instead of failing the whole batch. - `dry_run` (boolean | null, optional) - `clients` (array>, required, maxItems 500) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkCreateResult), required) — Result of a bulk invoice creation (`POST /v1/invoices/bulk-create`). `dry_run` reports the mode. In validate-only mode (`dry_run=true`) `results[]` carries the per-row classification `{index, status, errors[], warnings[]}` and nothing is persisted; in create mode (`dry_run=false`) only valid rows are created and `failures[]` carries the rows that were not created (identified by `index`). `total = successful + failed`. - `dry_run` (boolean, required) — Whether the operation ran in validate-only mode (no invoice was created). - `total` (integer, required) — Number of rows processed (`successful + failed`). - `successful` (integer, required) — Number of rows that validated successfully (`dry_run=true`) or were created (`dry_run=false`). - `failed` (integer, required) — Number of rows that were invalid or could not be created. Equals `failures` length in create mode. - `results` (array, required) — Per-row classification of validate-only mode (`dry_run=true`). Empty in create mode. - `failures` (array, required) — Rows that could not be created (create mode, `dry_run=false`). Each item carries the 0-based `index`, an `error_code`, a Spanish `error_message` and the per-field `errors[]`. Empty in validate-only mode. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients/bulk-delete — Delete multiple clients in bulk - **Operation ID**: `public-api.v1.clients.bulk_delete` - **Tag**: Clients - **Required scope**: `clients:delete` — Delete clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.bulk_delete Delete up to 200 clients in one request. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`); clients with associated documents are reported in `failures`. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Delete several clients in one request. `ids` is an array of 1 to 200 UUIDs; identifiers that do not belong to your company are reported under `failed` rather than failing the whole request. - `ids` (array, required, maxItems 200) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients — Create a client - **Operation ID**: `public-api.v1.clients.create` - **Tag**: Clients - **Required scope**: `clients:write` — Create and update clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.create Create a new client (customer) for your company. The returned object includes the generated `uuid` you should store for subsequent operations. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 30 properties; 1 required: `name`. - `name` (string, required, maxLength 200) - `commercial_name` (string | null, optional, maxLength 255) - `tax_id` (string | null, optional, maxLength 20) - `vat_id` (string | null, optional, maxLength 20) - `email` (string | null, optional, format: email, maxLength 191) - `phone` (string | null, optional, maxLength 20, pattern: `^\+?[0-9\s\-()]{6,20}$`) - `fax` (string | null, optional, maxLength 20) - `mobile` (string | null, optional, maxLength 20) - `website` (string | null, optional, format: uri, maxLength 255) - `contact_person` (string | null, optional, maxLength 200) - `latitude` (number | null, optional, min -90, max 90) - `longitude` (number | null, optional, min -180, max 180) - `default_discount` (number | null, optional, min 0, max 100) - `default_vat_rate` (number | null, optional, min 0, max 100) - `default_retention_rate` (number | null, optional, min -100, max 0) - `is_surcharge_subject` (boolean | null, optional) - `accumulate_347` (boolean, optional) - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`) - `payment_method` (string | null, optional, enum: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`) - `payment_terms_days` (integer | null, optional, min 0, max 365) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `dir3_accounting_office` (string | null, optional) - `dir3_managing_body` (string | null, optional) - `dir3_processing_unit` (string | null, optional) - `external_id` (string | null, optional, maxLength 100) - `billing_emails` (array | null, optional, maxItems 5) - `alternative_id` (object, optional) - `type` (string, optional, enum: `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`, `tax_id_foreign`, `national_id`) — Alternative identifier type from the AEAT L7 catalog. Legacy aliases (`tax_id_foreign`/`national_id`) are accepted on input for backward compatibility. - `value` (string, optional, maxLength 50, minLength 1) - `country_code` (string, optional, maxLength 2, minLength 2, pattern: `^[A-Z]{2}$`) - `address` (object, optional) - `line1` (string | null, optional, maxLength 500) - `line2` (string | null, optional, maxLength 100) - `number` (string | null, optional, maxLength 100) - `floor` (string | null, optional, maxLength 100) - `door` (string | null, optional, maxLength 100) - `staircase` (string | null, optional, maxLength 100) - `postal_code` (string | null, optional, maxLength 10) - `city` (string | null, optional, maxLength 100) - `province` (string | null, optional, maxLength 100) - `country` (string | null, optional, maxLength 2, minLength 2) - `bank_accounts` (array | null, optional) - `iban` (string, required, maxLength 50, pattern: `^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9 ]{11,42}$`) - `bic` (string | null, optional, maxLength 20, pattern: `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$`) - `is_default` (boolean | null, optional) - `notes` (string | null, optional, maxLength 255) ## Responses - **201** — Client created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Client), required) — A customer of your company. - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/clients/{client} — Delete a client - **Operation ID**: `public-api.v1.clients.delete` - **Tag**: Clients - **Required scope**: `clients:delete` — Delete clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.delete Delete a client. Returns 422 if the client is referenced by any document (invoice, quote, etc.). ## Path parameters - `client` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients/find-by-external-id — Find a client by external ID - **Operation ID**: `public-api.v1.clients.find_by_external_id` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.find_by_external_id Look up a client by their `external_id` (sent in the JSON body), the integration key that maps them to a record in a third-party system (ERP/CRM/e-commerce). Distinct from the fiscal `tax_id`. Returns the matching client or 404 if no client uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a client by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Client), required) — A customer of your company. - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients/find-by-tax-id — Find a client by tax ID - **Operation ID**: `public-api.v1.clients.find_by_tax_id` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.find_by_tax_id Look up a client by their Spanish tax identifier (NIF/CIF/NIE). Returns the matching client or 404 if no client uses that tax_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `tax_id`. Look up a client by its Spanish tax ID (NIF/CIF/NIE) within your company. - `tax_id` (string, required, maxLength 50) ## Responses - **200** - Body (`application/json`): - `data` (object (Client), required) — A customer of your company. - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients/import — Import clients from a file - **Operation ID**: `public-api.v1.clients.import` - **Tag**: Clients - **Required scope**: `clients:write` — Create and update clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.import Import clients in bulk from a CSV/XLSX file as `multipart/form-data`; processing is synchronous and the response carries the per-row outcome. Upload with `dry_run=true` first to validate without persisting, fix the reported `failures[]`, then re-upload with `dry_run=false` to create only the valid rows. `mapping` maps your column headers to the target fields (`name` and `tax_id` are mandatory). Download the header template from `GET /v1/clients/import-template`. ```json { "dry_run": true, "mapping": { "Nombre": "name", "CIF": "tax_id", "Email": "email" } } ``` Limits: file ≤10 MB and under 200 rows; a larger file returns 422 `client_import_too_large`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `multipart/form-data`, required. 3 properties; 2 required: `file`, `mapping`. Import clients from a file as `multipart/form-data`. `file` is a CSV/XLSX/XLS/ODS/TXT document (≤10 MB); `mapping` maps your column headers to target fields and must include at least `name` and `tax_id`; `dry_run` (default `false`) validates the file and returns a per-row preview without persisting. - `file` (string, required, format: binary, maxLength 10240) — CSV/XLSX/XLS/ODS/TXT file with the clients to import (max 10 MB). - `dry_run` (boolean | null, optional) — If `true`, validates the file and returns a per-row preview WITHOUT persisting any client. Defaults to `false`. - `mapping` (object, required) — Mapeo de columnas `{cabecera_csv: campo_destino}`. Debe declarar al menos `name` y `tax_id`. ## Responses - **200** - Body (`application/json`): - `data` (object (ClientImportPreview), required) — Validate-only preview of a client import (`POST /v1/clients/import` with `dry_run=true`). The CSV/XLSX file is checked row by row without persisting anything: `rows[]` carries the per-row classification `{row, status, errors[], warnings[]}` identified by the 1-based file `row` number (the header is row 1). - `object` (string, required, enum: `client_import_preview`) — Stripe-like discriminator. Always `client_import_preview` for this resource. - `total_rows` (integer, required) — Number of data rows in the uploaded file (excluding the header). - `rows` (array, required) — Per-row validation of the file. One item per data row, in file order. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/clients/import/template — Download the client import template - **Operation ID**: `public-api.v1.clients.import_template` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.import_template Download the CSV template (Spanish headers + two example rows) to fill in before uploading it to `POST /v1/clients/import`. The content is static and accesses no company data. Returns a `text/csv` stream as an attachment. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - object - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/clients — List all clients - **Operation ID**: `public-api.v1.clients.list` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.list List your clients with cursor-based pagination. Supports filtering by `is_active`, `created_at[gte|lte]`, and `name[in]`. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `tax_id` (string, optional) — Fiscal tax number (NIF/CIF/NIE) of the client. - `tax_id[in]` (string, optional) — Fiscal tax number (NIF/CIF/NIE) of the client. - `email` (string, optional, format: email) — Client email. - `name` (string, optional) — Trade name of the client. - `city` (string, optional) — City of the client postal address. - `city[contains]` (string, optional) — City of the client postal address. - `province` (string, optional) — Province / region of the client postal address. - `province[contains]` (string, optional) — Province / region of the client postal address. - `phone` (string, optional) — Phone number of the client. - `phone[contains]` (string, optional) — Phone number of the client. - `is_active` (boolean, optional) — Filter by active / inactive clients. - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `search` (string, optional, maxLength 80) — Free-text search. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/clients/search — Search clients - **Operation ID**: `public-api.v1.clients.search` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.search Search clients by free-text query against `name`, `tax_id`, `vat_id`, `email`, and `phone`. Returns a flat array (no pagination) capped at 50 results. ## Query parameters - `q` (string, required, maxLength 120, minLength 1) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/clients/{client} — Retrieve a client - **Operation ID**: `public-api.v1.clients.show` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.show Retrieve a client by its `uuid`. Returns 404 if the client does not exist or belongs to another company. ## Path parameters - `client` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Client), required) — A customer of your company. - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/clients/stats — Get client stats - **Operation ID**: `public-api.v1.clients.stats` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.stats Aggregated KPIs for the authenticated company: total client count, active count, count with sales invoices, count with quotes, and totals by document type. Returned as `{ "data": ClientStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ClientStats), required) — Aggregated summary of the client portfolio of the authenticated company: counters, invoiced and pending amounts, and average metrics. Returned by `GET /v1/clients/stats`. - `object` (string, required, enum: `client_stats`) - `total` (integer, required) — Total number of clients registered in the company. - `active` (integer, required) — Clientes marcados como activos. - `with_email` (integer, required) — Clients with a registered email address. - `with_phone` (integer, required) — Clients with a registered phone number. - `with_invoices` (integer, required) — Clients with at least one issued invoice. - `total_invoiced` (number, required) — Total amount invoiced to clients (EUR). - `total_pending` (number, required) — Total amount pending collection (EUR). - `with_pending_invoices` (integer, required) — Clients with at least one invoice pending collection. - `pending_invoices_amount` (number, required) — Aggregate amount of invoices pending collection (EUR). - `new_this_month` (integer, required) — Clientes nuevos creados durante el mes en curso. - `inactive_clients` (integer, required) — Clientes marcados como inactivos. - `average_per_client` (number, required) — Average amount invoiced per client (EUR). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/clients/{client} — Update a client - **Operation ID**: `public-api.v1.clients.update` - **Tag**: Clients - **Required scope**: `clients:write` — Create and update clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.update Update a client. Only fields included in the payload are modified; omitted fields retain their previous values. ## Path parameters - `client` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 30 properties; none of them required. Public REST API v1 — PUT /v1/clients/{uuid}. Partial PUT update: all fields are `sometimes`. If not sent, the handler keeps the current value. If sent as `null`, the field is cleared (when the domain allows it). Accepts the same fields as `CreateClientRequest` V1 (see docblock there). The validation of domain invariants (XOR `tax_id`/`alternative_id`, direct_debit ⇒ default bank account, billing_emails without duplicates) is performed by the `Client` aggregate. The typed exceptions propagate to the `ExceptionRenderer` with the canonical v1 envelope. - `name` (string, optional, maxLength 200) - `commercial_name` (string | null, optional, maxLength 255) - `tax_id` (string | null, optional, maxLength 20) - `vat_id` (string | null, optional, maxLength 20) - `email` (string | null, optional, format: email, maxLength 191) - `phone` (string | null, optional, maxLength 20, pattern: `^\+?[0-9\s\-()]{6,20}$`) - `fax` (string | null, optional, maxLength 20) - `mobile` (string | null, optional, maxLength 20) - `website` (string | null, optional, format: uri, maxLength 255) - `contact_person` (string | null, optional, maxLength 200) - `latitude` (number | null, optional, min -90, max 90) - `longitude` (number | null, optional, min -180, max 180) - `default_discount` (number | null, optional, min 0, max 100) - `default_vat_rate` (number | null, optional, min 0, max 100) - `default_retention_rate` (number | null, optional, min -100, max 0) - `is_surcharge_subject` (boolean | null, optional) - `accumulate_347` (boolean, optional) - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`) - `payment_method` (string | null, optional, enum: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`) - `payment_terms_days` (integer | null, optional, min 0, max 365) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `dir3_accounting_office` (string | null, optional) - `dir3_managing_body` (string | null, optional) - `dir3_processing_unit` (string | null, optional) - `external_id` (string | null, optional, maxLength 100) - `billing_emails` (array | null, optional, maxItems 5) - `alternative_id` (object, optional) - `type` (string, optional, enum: `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`, `tax_id_foreign`, `national_id`) — Alternative identifier type from the AEAT L7 catalog. Legacy aliases (`tax_id_foreign`/`national_id`) are accepted on input for backward compatibility. - `value` (string, optional, maxLength 50, minLength 1) - `country_code` (string, optional, maxLength 2, minLength 2, pattern: `^[A-Z]{2}$`) - `address` (object, optional) - `line1` (string | null, optional, maxLength 500) - `line2` (string | null, optional, maxLength 100) - `number` (string | null, optional, maxLength 100) - `floor` (string | null, optional, maxLength 100) - `door` (string | null, optional, maxLength 100) - `staircase` (string | null, optional, maxLength 100) - `postal_code` (string | null, optional, maxLength 10) - `city` (string | null, optional, maxLength 100) - `province` (string | null, optional, maxLength 100) - `country` (string | null, optional, maxLength 2, minLength 2) - `bank_accounts` (array | null, optional) - `iban` (string, required, maxLength 50, pattern: `^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9 ]{11,42}$`) - `bic` (string | null, optional, maxLength 20, pattern: `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$`) - `is_default` (boolean | null, optional) - `notes` (string | null, optional, maxLength 255) ## Responses - **200** - Body (`application/json`): - `data` (object (Client), required) — A customer of your company. - `id` (string, required) - `object` (string, required, enum: `client`) - `name` (string, required) - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Client website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the client. `null` when not recorded. - `default_discount` (number | null, optional, format: float) — Default discount applied to the client (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the client (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al cliente se le aplica recargo de equivalencia. - `bank_accounts` (array, optional) — Bank accounts associated with the client. Empty `[]` when there are none. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the client for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this client accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `dir3_accounting_office` (string | null, required) — DIR3 code of the public-administration accounting office (Oficina Contable). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_managing_body` (string | null, required) — DIR3 code of the public-administration managing body (Órgano Gestor). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `dir3_processing_unit` (string | null, required) — DIR3 code of the public-administration processing unit (Unidad Tramitadora). FACe/Facturae directory code, not a foreign key. `null` for non-AAPP clients. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this client to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/clients/census-verification — Verify a client against the AEAT census - **Operation ID**: `public-api.v1.clients.verify_census` - **Tag**: Clients - **Required scope**: `clients:read` — Read clients. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/clients/public-api.v1.clients.verify_census Check a third-party name + tax ID pair (the recipient of an invoice) against the AEAT census (VNifV2) to anticipate VeriFactu 1239 rejections before invoicing. Stateless and informational: nothing is persisted on the client. Fail-open — if AEAT is unreachable the call returns 200 with `status: unavailable`. Test keys (`fact_test_`) return deterministic statuses per magic NIF without contacting AEAT. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `tax_id`, `name`. Public REST API v1 — POST /v1/clients/census-verification. Verifies a THIRD PARTY's name + tax_id pair (invoice recipient) against the AEAT census (VNifV2). Max lengths mirror the Company BC value objects consumed by the bridge (`TaxIdentifier` ≤ 20, `CompanyName` ≤ 100). - `tax_id` (string, required, maxLength 20) — Spanish tax identifier (NIF/CIF/NIE) of the third party to check against the AEAT census - `name` (string, required, maxLength 100) — Name or business name of the third party (the name + tax ID pair is verified together) ## Responses - **200** - Body (`application/json`): - `data` (object (CensusVerification), required) — Result of verifying the account's persisted company name + tax ID pair against the AEAT census (VNifV2). Use it to anticipate VeriFactu 4104 rejections before invoicing. Fail-open: returns `unavailable` when AEAT cannot be reached. - `object` (string, required, enum: `census_verification`) — Stripe-like discriminator. Always `census_verification` for this resource. - `status` (string, required, enum: `identified`, `not_identified`, `not_identified_similar`, `identified_inactive`, `identified_revoked`, `unavailable`) — Census result. `identified`: name + tax ID match an active taxpayer. `not_identified`: the pair is not in the census. `not_identified_similar`: a similar individual exists (natural persons only). `identified_inactive` / `identified_revoked`: the taxpayer is deregistered or revoked. `unavailable`: AEAT could not answer (timeout, fault, no platform certificate) — verification is informational and never blocks. - `verified_name` (string | null, required) — The company name that was checked against the census (the persisted account name). `null` when the account has never been verified. - `checked_at` (string | null, required, format: date-time) — ISO 8601 timestamp of the last verification. `null` when never verified. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies/{company}/activate — Activate a managed company - **Operation ID**: `public-api.v1.companies.activate` - **Tag**: Companies - **Required scope**: `companies:write` — Create and update companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.activate Reactivate a previously deactivated (`inactive`) managed company. Activation is gated by an atomic per-seat charge — in live mode the prorated seat is charged synchronously and the company only becomes `active` if the charge succeeds. No payment method on file returns 402, and a plan without the gestoría module returns 403. Trial, enterprise and test keys skip the charge. ## Path parameters - `company` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies/activate — Activate several managed companies - **Operation ID**: `public-api.v1.companies.activate_batch` - **Tag**: Companies - **Required scope**: `companies:write` — Create and update companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.activate_batch Reactivate several deactivated (`inactive`) managed companies in one operation, charging the combined prorated seats in a single invoice. Pass `company_ids`. The gate is atomic: every company is validated (ownership and `inactive` status) before any charge, so if one is invalid the whole batch is rejected without charging or activating any. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `company_ids`. Reactivate several managed companies in a single request. `company_ids` is a list of company IDs (UUID v7) to reactivate, between 1 and 1000. Ownership of each company and its required `inactive` status are enforced server-side; companies that are not yours or not inactive are reported per item without affecting the rest. - `company_ids` (array, required, maxItems 1000) — List of managed child company IDs (UUID v7) to reactivate in bulk (between 1 and 1000). ## Responses - **200** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies/{company}/api-keys — Create a child API key - **Operation ID**: `public-api.v1.companies.api_keys.create` - **Tag**: Companies - **Required scope**: `api_keys:write` — Create and update api keys. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.api_keys.create Create an API key scoped to one of your managed companies and return its plaintext `secret` exactly once — store it now, it cannot be retrieved later. The requested scopes must be a subset of the calling key's scopes; requesting a scope the parent key does not hold returns 422 (no silent narrowing). ## Path parameters - `company` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 4 properties; 2 required: `name`, `scopes`. Issue an API key scoped to one of your managed child companies. The plaintext secret is returned once in the creation response. Requested `scopes` must belong to the closed v1 catalog and be a subset of the calling key scopes; the environment and tier are never accepted from the body. - `name` (string, required, maxLength 120, minLength 1) — Human-readable name for the API key (1-120 characters). - `expires_at` (string | null, optional, format: date-time) — Future ISO 8601 date after which the key stops authenticating. - `scopes` (array, required) — List of scopes from the closed v1 catalog (at least one; a subset of the parent key scopes). - `ip_allowlist` (array | null, optional) — Optional list of allowed IPs / CIDR ranges (IPv4, IPv6, /N). ## Responses - **201** - Body (`application/json`): - `data` (object (ApiKeyWithSecret), required) — An API key returned once at creation or after secret rotation. Includes the plaintext `secret` — store it now, it cannot be retrieved later. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - `secret` (string, required) — Plaintext secret. Returned only at creation or after rotation — never again. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed — e.g. the API key plan limit was reached, or an invoice language outside the allowed catalog (`es`, `en`, `ca`). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/companies/{company}/api-keys — List child API keys - **Operation ID**: `public-api.v1.companies.api_keys.list` - **Tag**: Companies - **Required scope**: `api_keys:read` — Read api keys. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.api_keys.list List the API keys of one of your managed companies with cursor-based pagination, including revoked keys for audit. The plaintext secret is never returned. A company not managed by your master tenant returns 404. ## Path parameters - `company` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `ApiKeyListV1Resource` - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/companies/{company}/api-keys/{api_key} — Revoke a child API key - **Operation ID**: `public-api.v1.companies.api_keys.revoke` - **Tag**: Companies - **Required scope**: `api_keys:write` — Create and update api keys. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.api_keys.revoke Revoke a child API key immediately and irreversibly, leaving it unusable. Subsequent requests authenticated with that key fail with 401. A company not managed by your master tenant returns 404. ## Path parameters - `company` (string, required) - `api_key` (string, required) ## Query parameters - `reason` (string | null, optional, maxLength 500) — Motivo opcional de la revocación (queda en audit log). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiKey), required) — An API key of your company. The plaintext secret is never exposed in this representation — it is shown only once, at creation or after rotation. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed — e.g. the API key plan limit was reached, or an invoice language outside the allowed catalog (`es`, `en`, `ca`). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies/{company}/api-keys/{api_key}/rotate-secret — Rotate a child API key secret - **Operation ID**: `public-api.v1.companies.api_keys.rotate_secret` - **Tag**: Companies - **Required scope**: `api_keys:write` — Create and update api keys. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.api_keys.rotate_secret Invalidate the current secret of a child API key immediately, generate a fresh `prefix` + `secret`, and return the new secret in plaintext exactly once. Any request made with the previous secret stops authenticating right away. Irreversible. A company not managed by your master tenant returns 404. ## Path parameters - `company` (string, required) - `api_key` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiKeyWithSecret), required) — An API key returned once at creation or after secret rotation. Includes the plaintext `secret` — store it now, it cannot be retrieved later. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - `secret` (string, required) — Plaintext secret. Returned only at creation or after rotation — never again. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/companies/{company}/api-keys/{api_key} — Retrieve a child API key - **Operation ID**: `public-api.v1.companies.api_keys.show` - **Tag**: Companies - **Required scope**: `api_keys:read` — Read api keys. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.api_keys.show Retrieve a single API key of one of your managed companies by its `id` (UUID v7). The plaintext secret is never included. A key not belonging to a company you manage returns 404 `api_key_not_found` (anti-enumeration). ## Path parameters - `company` (string, required) - `api_key` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiKey), required) — An API key of your company. The plaintext secret is never exposed in this representation — it is shown only once, at creation or after rotation. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the API key. - `object` (string, required, enum: `api_key`) — Always `api_key`. - `name` (string, required) — Human-friendly label assigned at creation time. - `prefix` (string, required) — First chars of the key (e.g. `fact_live_1N0Fnyhh`) — safe to log. Does NOT authenticate. - `scopes` (array, required) — Authorized scopes. `*` means super scope (full access). - `tier` (string, required) — Rate-limit tier (`free`, `starter`, `pro`, `scale`). Derived from the company plan (or from an active capacity boost when higher), never set from the request body. - `environment` (string, required, enum: `live`, `test`) — Key environment: `live` (`fact_live_`, real side effects) or `test` (`fact_test_`, sandbox company, no real-world effects). - `created_at` (string, required, format: date-time) - `last_used_at` (string | null, required, format: date-time) — Timestamp of the last authenticated request with this key, or `null` if never used. - `expires_at` (string | null, required, format: date-time) — Expiry instant (ISO 8601), or `null` if the key does not expire. - `revoked_at` (string | null, required, format: date-time) — Revocation instant (ISO 8601), or `null` if the key is not revoked. - `is_active` (boolean, required) — `true` when the key is usable (not revoked and not expired). - `is_revoked` (boolean, required) — `true` once the key has been revoked. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies — Create a managed company - **Operation ID**: `public-api.v1.companies.create` - **Tag**: Companies - **Required scope**: `companies:write` — Create and update companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.create Register a new managed company (a child sub-account) under your master tenant — the gestoría model. `name` and `tax_id` are required, and `tax_id` must be unique among the companies you manage (a duplicate returns 409). In live mode the prorated per-seat charge gates creation: with no payment method on file or a failed charge the call returns 402 and nothing is created. Use `GET /v1/companies/seat-charge-preview` to anticipate the cost; test keys skip the charge. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 10 properties; 2 required: `name`, `tax_id`. Register a managed child company under your master tenant. `name` and `tax_id` are required; the rest of the profile (business name, fiscal address, contact details) is optional. `tax_id` is validated as a Spanish tax ID (NIF/CIF/NIE) and must be unique among the companies you manage; `country_aeat_zone` is derived from the postal code. - `name` (string, required, maxLength 255, minLength 1) — Trade name of the child company (1-255 characters). - `tax_id` (string, required, maxLength 20) — Spanish tax identifier (NIF, CIF or NIE). Immutable after creation. - `business_name` (string | null, optional, maxLength 100) — Legal/registered business name of the child company. - `address` (string | null, optional, maxLength 255) — Fiscal address. - `city` (string | null, optional, maxLength 100) — City of the fiscal address. - `postal_code` (string | null, optional, maxLength 10) — Postal code (derives the AEAT zone). - `province` (string | null, optional, maxLength 100) — Province. - `country` (string | null, optional, maxLength 100) — Country. - `email` (string | null, optional, format: email, maxLength 255) — Contact email of the child company. - `phone` (string | null, optional, maxLength 20) — Contact phone number. ## Responses - **201** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/companies/{company}/creation-status — Retrieve the creation status of a managed company - **Operation ID**: `public-api.v1.companies.creation_status` - **Tag**: Companies - **Required scope**: `companies:read` — Read companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.creation_status Poll the provisioning lifecycle of a managed company. Returns `provisioning_status` (`pending`, `awaiting_payment`, `provisioning`, `active`, `failed`). `payment_setup_url` is present only while `awaiting_payment` and points to the master tenant's payment-method onboarding; `failed_reason` is present only when provisioning has `failed`. Test keys move the child to `active` directly. ## Path parameters - `company` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (CompanyCreationStatus), required) — Provisioning lifecycle status of a managed company (child sub-account) created through the gestoría endpoints. Poll it after creating a company to know when it becomes operational. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company_creation_status`) — Always `company_creation_status`. - `provisioning_status` (string, required, enum: `pending`, `awaiting_payment`, `provisioning`, `active`, `failed`) — Provisioning lifecycle status: `pending`, `awaiting_payment`, `provisioning`, `active` or `failed`. - `payment_setup_url` (string | null, required, format: uri) — URL to onboard the master tenant's payment method. Only present while `awaiting_payment`; `null` otherwise. - `failed_reason` (string | null, required) — Human-readable reason the provisioning failed. Only present when `provisioning_status` is `failed`; `null` otherwise. - `started_at` (string | null, required, format: date-time) — When provisioning started (ISO 8601), or `null` if not started yet. - `completed_at` (string | null, required, format: date-time) — When provisioning completed (ISO 8601), or `null` if not completed yet. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies/{company}/deactivate — Deactivate a managed company - **Operation ID**: `public-api.v1.companies.deactivate` - **Tag**: Companies - **Required scope**: `companies:write` — Create and update companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.deactivate Deactivate a managed company, moving it from `active` to `inactive`: it becomes non-operational but its data is preserved and the change is reversible (reactivate it later by paying its seat). No charge is applied; instead a prorated seat credit is emitted best-effort for the unused time. ## Path parameters - `company` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/companies/{company} — Archive a managed company - **Operation ID**: `public-api.v1.companies.delete` - **Tag**: Companies - **Required scope**: `companies:delete` — Delete companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.delete Archive a managed company, moving it to the `archived` status so it no longer accepts operations. The underlying company row and its history are preserved. Archiving may be blocked by business rules (returns 422 `business_rule_violation`). A company not managed by your master tenant returns 404. ## Path parameters - `company` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/companies — List your managed companies - **Operation ID**: `public-api.v1.companies.list` - **Tag**: Companies - **Required scope**: `companies:read` — Read companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.list List the companies managed by your master tenant with cursor-based pagination. By default only `active` and `inactive` companies are returned; pass `status` (`active`, `inactive`, `archived`) to filter — `status=archived` is the opt-in way to surface archived companies. Only your own children are ever returned. ## Query parameters - `status` (string | null, optional, enum: `active`, `inactive`, `archived`) — Filtrar por estado del vínculo de gestoría. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `CompanyListV1Resource` - Body (`application/json`): - `data` (array, required) — Page of managed companies. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - `has_more` (boolean, required) — `true` when more companies exist beyond this page. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/companies/seat-charge-preview — Preview the seat charge of adding a company - **Operation ID**: `public-api.v1.companies.seat_charge_preview` - **Tag**: Companies - **Required scope**: `companies:read` — Read companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.seat_charge_preview Preview the prorated per-seat amount for adding or activating managed companies, computed from the master tenant's Stripe upcoming invoice, without charging. Use `count` (≥1) to preview a batch, or `company_ids` for a coverage-aware preview: companies still covered for the current period cost `0` (`already_covered: true`). `amount` is in the currency's minor units; `requires_payment_method` is `true` when no payment method is on file. ## Query parameters - `count` (integer | null, optional, min 1, max 1000) — Número de empresas hijas que se activarían en bloque (≥1, default 1). - `company_ids[]` (array, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/companies/{company} — Retrieve a managed company - **Operation ID**: `public-api.v1.companies.show` - **Tag**: Companies - **Required scope**: `companies:read` — Read companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.show Retrieve a single managed company by its `id` (UUID v7). A company not managed by your master tenant returns 404 `company_not_found` (anti-enumeration). ## Path parameters - `company` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PATCH /v1/companies/{company} — Update a managed company - **Operation ID**: `public-api.v1.companies.update` - **Tag**: Companies - **Required scope**: `companies:write` — Create and update companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.update Update the profile of a managed company (`name`, `business_name`, address fields, `email`, `phone`). The `tax_id` is immutable after creation (sending it returns 422) and `country_aeat_zone` is derived from the address. Partial update: omitted fields keep their value; send `""` to clear a field. ## Path parameters - `company` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 9 properties; none of them required. Partial update of a managed child company profile (business name, fiscal address, contact details). Omitted fields keep their current value; send `""` to clear a field. `country_aeat_zone` is derived from the postal code, and `tax_id` is immutable after creation (sending it returns 422). - `name` (string, optional, maxLength 255, minLength 1) — Trade name of the child company (1-255 characters). - `business_name` (string | null, optional, maxLength 100) — Legal/registered business name of the child company. - `address` (string | null, optional, maxLength 255) — Fiscal address. - `city` (string | null, optional, maxLength 100) — City of the fiscal address. - `postal_code` (string | null, optional, maxLength 10) — Postal code (derives the AEAT zone). - `province` (string | null, optional, maxLength 100) — Province. - `country` (string | null, optional, maxLength 100) — Country. - `email` (string | null, optional, format: email, maxLength 255) — Contact email of the child company. - `phone` (string | null, optional, maxLength 20) — Contact phone number. ## Responses - **200** - Body (`application/json`): - `data` (object (Company), required) — A managed company (child sub-account) under your master tenant. Created and operated through the gestoría endpoints; its child API keys can only hold a subset of the scopes of the key that created them. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company`) — Always `company`. - `name` (string, required) — Commercial name of the managed company. - `business_name` (string | null, required) — Legal/registered business name (razón social), or `null` if not set. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF/NIE). Unique across the companies managed by your master tenant. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (operational) or `archived` (no longer accepts operations). - `address` (string | null, required) — Street address of the fiscal domicile, or `null`. - `city` (string | null, required) — City of the fiscal domicile, or `null`. - `postal_code` (string | null, required) — Postal code of the fiscal domicile, or `null`. - `province` (string | null, required) — Province of the fiscal domicile, or `null`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — AEAT territorial zone of the company (`peninsula`, `canarias`, `ceuta`, `melilla`), or `null`. - `email` (string | null, required, format: email) — Contact email, or `null`. - `phone` (string | null, required) — Contact phone, or `null`. - `logo_url` (string | null, required, format: uri) — Absolute URL of the company logo, or `null` if not set. - `seat_paid_until` (string | null, required, format: date-time) — Paid seat coverage of this managed company (ISO 8601): reactivating it before this date is free. `null` when the seat was never charged or the coverage expired. - `created_at` (string, required, format: date-time) — When the managed company was registered (ISO 8601). - `updated_at` (string | null, required, format: date-time) — When the managed company was last updated (ISO 8601), or `null`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/companies/{company}/verify-creation — Verify the creation of a managed company - **Operation ID**: `public-api.v1.companies.verify_creation` - **Tag**: Companies - **Required scope**: `companies:write` — Create and update companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.companies.verify_creation Reconcile and advance the provisioning of a managed company against the master tenant's subscription. No request body; idempotent. While `awaiting_payment`, once the master has a payment method on file the child is charged the prorated seat and moves to `active`; otherwise it stays `awaiting_payment` with no error. Returns the creation-status resource. ## Path parameters - `company` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (CompanyCreationStatus), required) — Provisioning lifecycle status of a managed company (child sub-account) created through the gestoría endpoints. Poll it after creating a company to know when it becomes operational. - `id` (string, required, format: uuid) — Opaque identifier (UUID v7) of the managed company. - `object` (string, required, enum: `company_creation_status`) — Always `company_creation_status`. - `provisioning_status` (string, required, enum: `pending`, `awaiting_payment`, `provisioning`, `active`, `failed`) — Provisioning lifecycle status: `pending`, `awaiting_payment`, `provisioning`, `active` or `failed`. - `payment_setup_url` (string | null, required, format: uri) — URL to onboard the master tenant's payment method. Only present while `awaiting_payment`; `null` otherwise. - `failed_reason` (string | null, required) — Human-readable reason the provisioning failed. Only present when `provisioning_status` is `failed`; `null` otherwise. - `started_at` (string | null, required, format: date-time) — When provisioning started (ISO 8601), or `null` if not started yet. - `completed_at` (string | null, required, format: date-time) — When provisioning completed (ISO 8601), or `null` if not completed yet. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/gestoria/workforce-summary — Retrieve the consolidated workforce compliance overview - **Operation ID**: `public-api.v1.gestoria.workforce_summary` - **Tag**: Companies - **Required scope**: `companies:read` — Read companies. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/companies/public-api.v1.gestoria.workforce_summary Return the consolidated time-tracking compliance panel for your whole managed portfolio: one row per `active` managed company, each projected from that company's latest monthly close without recomputation — whether the current (last closable) period is closed, its status (`closed`/`reopened`), the last closed period (`last_closed_year`/`last_closed_month`), and the aggregated `total_balance_minutes`, `total_overtime_minutes` and `employee_count`. Master-scoped: the portfolio is resolved from your API key, never from the payload, and only your own children appear. Unlike the per-company `X-Active-Profile` endpoints, this aggregates across children in a single call. Returned as `{ "data": [ConsolidatedWorkforce, ...] }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — UUID v7 of the managed company this row belongs to (the resource has no id of its own). - `name` (string, required) — Trade name / legal name of the managed company. - `tax_id` (string, required) — Spanish fiscal identifier (NIF/CIF) of the managed company. - `current_period_closed` (boolean, required) — true when the current (last closable) period is closed for this company. - `current_period_status` (string | null, required, enum: `closed`, `reopened`, `null`) — Status of the current period close: `closed`, `reopened`, or `null` when the company has no close for it. - `last_closed_year` (integer | null, required) — Year of the company’s latest existing monthly close, or `null` when it has none. - `last_closed_month` (integer | null, required) — Month (1-12) of the company’s latest existing monthly close, or `null` when it has none. - `total_balance_minutes` (integer, required) — Total balance in minutes from the snapshot of the latest close (may be negative). - `total_overtime_minutes` (integer, required) — Total overtime in minutes from the snapshot of the latest close. - `employee_count` (integer, required) — Number of employees in the snapshot of the latest close. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/bulk-delete — Bulk delete delivery notes - **Operation ID**: `public-api.v1.delivery_notes.bulk_delete` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:delete` — Delete delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.bulk_delete Delete several delivery notes in a single request. The body takes an `ids` array of `uuid`s. Returns a `BulkPartialSuccessResult` with `total`, `successful`, `failed` counts and a `failures` list (`id` + `error_code` + Spanish `error_message`) for those that could not be deleted (e.g. signed or invoiced). Supports `Idempotency-Key` for safe retries. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Delete several delivery notes in one request. `ids` is an array of 1 to 100 UUIDs; unknown or cross-tenant identifiers are reported as failed rather than failing the whole request. - `ids` (array, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/bulk-pdf — Bulk download delivery note PDFs - **Operation ID**: `public-api.v1.delivery_notes.bulk_pdf` - **Tag**: Delivery Notes - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.bulk_pdf Packages the PDFs of up to 50 delivery notes (by id) into a single ZIP. Ids that are not found or have no generable PDF do not abort the request: the ZIP carries only the valid ones and the per-resource counts travel in the `X-Bulk-*` response headers. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Package the PDFs of several delivery notes into a single ZIP. `ids` is an array of delivery-note UUIDs, up to 50 per request. - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/zip`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/bulk-send — Bulk send delivery notes - **Operation ID**: `public-api.v1.delivery_notes.bulk_send` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:write` — Create and update delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.bulk_send Sends up to 200 delivery notes by email (queued) in one call, reusing the single-send path per id. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each delivery note that could not be sent (not found, non-sendable status or no resolvable recipient). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 1 required: `ids`. Email several delivery notes in one request (queued), up to 200 per batch. `ids` is an array of delivery-note UUIDs; the optional `to`/`cc` arrays and `subject`/`message`/`language` overrides apply to the whole batch (when `to` is omitted, each delivery note uses its client email). - `subject` (string | null, optional, maxLength 200) - `message` (string | null, optional, maxLength 5000) - `language` (string | null, optional, maxLength 5) - `ids` (array, required, maxItems 200) - `to` (array | null, optional) - `cc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/bulk-status — Bulk change delivery note status - **Operation ID**: `public-api.v1.delivery_notes.bulk_status` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:transition` — Change the lifecycle status of delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.bulk_status Transition up to 50 delivery notes (by id) to a status from the closed set `[delivered, cancelled]`, each through the document state guard. Returns a `BulkPartialSuccessResult`; delivery notes whose transition is rejected (not found or not transitionable) come back in `failures[]`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `new_status`, `ids`. Transition several delivery notes to `new_status` (`delivered` or `cancelled`) in one request, up to 50 per batch. `ids` is an array of delivery-note UUIDs; every transition passes the document state guard, and notes that cannot transition are returned under `failures[]`. - `new_status` (string, required, enum: `delivered`, `cancelled`) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/{delivery_note}/cancel — Cancel a delivery note - **Operation ID**: `public-api.v1.delivery_notes.cancel` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:transition` — Change the lifecycle status of delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.cancel Transition a delivery note to the `cancelled` state. Canonical REST replacement for the deprecated `POST /change_status`. Returns 409 `invalid_status_transition` if the note cannot be cancelled (e.g. already invoiced). Supports `Idempotency-Key` for safe retries. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/{delivery_note}/convert — Convert delivery note to invoice - **Operation ID**: `public-api.v1.delivery_notes.convert` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:transition` — Change the lifecycle status of delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.convert Convert a delivery note into a sales invoice. The delivery note moves to `invoiced` with `converted_to_id` populated and the new invoice is returned under `data`. Only `target=invoice` is supported. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `target`. - `target` (string, required, enum: `invoice`) - `target_series_id` (string | null, optional, format: uuid) ## Responses - **201** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes — Create a delivery note - **Operation ID**: `public-api.v1.delivery_notes.create` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:write` — Create and update delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.create Create a new delivery note (albarán) in `draft` status. Delivery notes track goods shipped to a customer and can later be converted to invoices. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 26 properties; 2 required: `client_id`, `lines`. - `client_id` (string, required, format: uuid) - `series_id` (string | null, optional, format: uuid) - `delivery_date` (string | null, optional, format: date) - `notes` (string | null, optional, maxLength 2000) - `internal_notes` (string | null, optional, maxLength 2000) - `reference_number` (string | null, optional, maxLength 255) - `transport_details` (string | null, optional, maxLength 2000) - `delivery_address` (string | null, optional, maxLength 500) - `delivery_city` (string | null, optional, maxLength 255) - `delivery_postal_code` (string | null, optional, maxLength 20) - `delivery_province` (string | null, optional, maxLength 255) - `delivery_country` (string | null, optional, maxLength 255) - `vehicle_plate` (string | null, optional, maxLength 20) — Licence plate of the vehicle used for the delivery (up to 20 characters). - `driver_name` (string | null, optional, maxLength 120) - `driver_tax_id` (string | null, optional, maxLength 20) - `tracking_number` (string | null, optional, maxLength 100) - `carrier_company` (string | null, optional, maxLength 120) - `received_by_name` (string | null, optional, maxLength 120) - `received_by_tax_id` (string | null, optional, maxLength 20) - `external_id` (string | null, optional, maxLength 100) - `currency` (string | null, optional, enum: `EUR`) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `billing_emails` (array | null, optional, maxItems 5) - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, required) - `description` (string, required, maxLength 255) - `quantity` (number, required, min 0.01) - `unit_price` (number, required, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **201** — Delivery note created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/delivery_notes/{delivery_note} — Delete a delivery note - **Operation ID**: `public-api.v1.delivery_notes.delete` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:delete` — Delete delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.delete Delete a delivery note. Only `draft` notes without an assigned number can be deleted; any other state returns 409 `invalid_status_transition`. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/{delivery_note}/duplicate — Duplicate a delivery note - **Operation ID**: `public-api.v1.delivery_notes.duplicate` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:write` — Create and update delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.duplicate Create a new draft delivery note by copying lines, client, and metadata. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/find-by-external-id — Find a delivery note by external ID - **Operation ID**: `public-api.v1.delivery_notes.find_by_external_id` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:read` — Read delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.find_by_external_id Look up a single delivery note by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Returns the matching delivery note or 404 `delivery_note_not_found` if no delivery note uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a delivery note by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/delivery_notes — List all delivery notes - **Operation ID**: `public-api.v1.delivery_notes.list` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:read` — Read delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.list List your delivery notes with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Public delivery note status: `draft`, `sent`, `signed`, `invoiced`, `cancelled` (the same values returned by the resource `status` field). - `status[in]` (string, optional) — Public delivery note status: `draft`, `sent`, `signed`, `invoiced`, `cancelled` (the same values returned by the resource `status` field). - `client_id` (string, optional, format: uuid) — Client ID (UUID v7). - `client_id[in]` (string, optional) — Client ID (UUID v7). - `series_id` (string, optional, format: uuid) — Series ID (UUID v7). - `series_id[in]` (string, optional) — Series ID (UUID v7). - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `delivery_date[gte]` (string, optional, format: date) — Delivery date. - `delivery_date[lte]` (string, optional, format: date) — Delivery date. - `delivery_date[gt]` (string, optional, format: date) — Delivery date. - `delivery_date[lt]` (string, optional, format: date) — Delivery date. - `signed` (boolean, optional) — Filter by signed delivery notes (`true`) or with no recorded signature (`false`). - `number` (string, optional) — Delivery note number. - `number[contains]` (string, optional) — Delivery note number. - `vehicle_plate` (string, optional) — License plate of the delivery vehicle. - `vehicle_plate[contains]` (string, optional) — License plate of the delivery vehicle. - `carrier_company` (string, optional) — Transport / courier company. - `carrier_company[contains]` (string, optional) — Transport / courier company. - `tags` (string, optional) — Filter by classification tag (lowercase slug). - `tags[in]` (string, optional) — Filter by classification tag (lowercase slug). - `sort` (string, optional, enum: `created`, `-created`, `number`, `-number`, `delivery_date`, `-delivery_date`) — Sort order. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/{delivery_note}/mark-delivered — Mark delivery note as delivered - **Operation ID**: `public-api.v1.delivery_notes.mark_delivered` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:transition` — Change the lifecycle status of delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.mark_delivered Transition a delivery note to the `delivered` state (public `sent`). Canonical REST replacement for the deprecated `POST /change_status`. Returns 409 `invalid_status_transition` if the note cannot transition. Supports `Idempotency-Key` for safe retries. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. Public REST API v1 — POST /v1/delivery_notes/{uuid}/mark-delivered. REST sub-resource that transitions the delivery note `draft → delivered`. Optional body: `delivery_date` (ISO 8601 `YYYY-MM-DD`). If omitted, the BC uses the delivery date already recorded or, failing that, the current date. - `delivery_date` (string | null, optional, format: date-time) ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/delivery_notes/{delivery_note}/pdf — Download delivery note PDF - **Operation ID**: `public-api.v1.delivery_notes.pdf` - **Tag**: Delivery Notes - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.pdf Download the PDF representation of a delivery note. Returns the binary PDF stream (`application/pdf`). Pass `?download=1` for `Content-Disposition: attachment` (file download); otherwise it is served `inline`. The response carries an `ETag`; resend it via `If-None-Match` to receive `304 Not Modified` when the document is unchanged. ## Path parameters - `delivery_note` (string, required) ## Query parameters - `download` (string, optional) — Cuando es truthy (`1`/`true`), fuerza `Content-Disposition: attachment` (descarga de fichero) en lugar de `inline`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — Binary PDF stream of the delivery note (`application/pdf`). Use `?download=1` to receive `Content-Disposition: attachment` (forces a file download); otherwise the disposition is `inline`. The response carries an `ETag`; send it back via `If-None-Match` to get a `304 Not Modified` when the PDF has not changed. - Body (`application/pdf`): - string - **304** - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/delivery_notes/{delivery_note}/public-link — Retrieve a delivery note public link - **Operation ID**: `public-api.v1.delivery_notes.public_link.get` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:read` — Read delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.public_link.get Return the public share link state of a delivery note: `url` (absolute, ready to send to the client), `enabled`, `expires_at` (`null` = unlimited), and `max_days` (plan-enforced maximum when extending the link). ## Path parameters - `delivery_note` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/delivery_notes/{delivery_note}/public-link — Update a delivery note public link - **Operation ID**: `public-api.v1.delivery_notes.public_link.update` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:write` — Create and update delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.public_link.update Enable/disable the public share link of a delivery note or change its expiry. Returns 422 `expiry_exceeds_max_days` if the requested expiry exceeds the plan-enforced `max_days`. Supports `Idempotency-Key` for safe retries. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `action`. Public REST API v1 — PUT /v1/delivery_notes/{uuid}/public-link. Required body: `action` ∈ {revoke, activate, extend, reset}. `extend_days` is required when `action=extend`. - `action` (string, required, enum: `revoke`, `activate`, `extend`, `reset`) - `extend_days` (integer, optional, min 1, max 36500) ## Responses - **200** — Updated state of the delivery note public link after applying the requested action (`revoke`, `activate`, `extend` or `reset`). Returns the canonical `url`, whether the link is `enabled`, its `expires_at` and the plan `max_days` cap. - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/{delivery_note}/send — Send a delivery note - **Operation ID**: `public-api.v1.delivery_notes.send` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:write` — Create and update delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.send Send a delivery note to the client by email. Uses the email on file unless overridden in the payload. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 4 properties; 1 required: `email`. Public REST API v1 — POST /v1/delivery_notes/{uuid}/send. Required body: `email`. Optional: `subject`, `message` (max 2000 chars), `template_id` (catalog id of the email template). `template_id` is the integer identifier of the global system table `templates` (shared catalog, without `company_id` or `uuid` column). It is validated against the PK `id`, like the internal SPA endpoint. That is why it does NOT follow the public UUID convention of the other FKs. - `email` (string, required, format: email) - `subject` (string | null, optional, maxLength 255) - `message` (string | null, optional, maxLength 2000) - `template_id` (integer | null, optional) — uuid-audit-allow: global system catalog (`templates` table without company_id or uuid column; integer catalog PK, like the internal SPA). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `object` (string, required, const: `delivery_note.send_result`) - `sent` (boolean, required) - `recipient` (string, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/delivery_notes/{delivery_note} — Retrieve a delivery note - **Operation ID**: `public-api.v1.delivery_notes.show` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:read` — Read delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.show Retrieve a delivery note by its `uuid`. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/{delivery_note}/sign — Sign a delivery note - **Operation ID**: `public-api.v1.delivery_notes.sign` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:transition` — Change the lifecycle status of delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.sign Record a handwritten signature on a delivery note, typically captured from the recipient on delivery. The signature must be a base64-encoded PNG (≤2 MB); other formats return 422. Signing sets `signed_at`/`signed_by` but does not change the status. The signature audit log retains hashed recipient PII for 5 years (Spanish LSSI-CE); use the `signature-audits/{auditId}/forget` endpoint to honor a GDPR erasure request. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 4 properties; 3 required: `signed_by`, `recipient_dni`, `signature_image_base64`. Public REST API v1 — POST /v1/delivery_notes/{uuid}/sign. Optional body: `signed_by` (alias of `recipient_name`, BC invariant), `recipient_dni` (BC invariant — Spanish DNI/NIE, required by `SignatureData`), `signature_image_base64` (raw base64 PNG; decode + size + magic bytes are validated by the controller to respond 422 `payload_too_large`/`invalid_param_format`), `signed_at` (ISO 8601 optional, default now). The controller converts `signed_by` → `recipient_name` and prefixes the base64 with `data:image/png;base64,` before dispatching to the BC. - `signed_by` (string, required, maxLength 255) - `recipient_dni` (string, required, maxLength 20) - `signature_image_base64` (string, required) - `signed_at` (string | null, optional, format: date-time) ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/delivery_notes/signature-audits/{auditId}/forget — Forget delivery note signature PII - **Operation ID**: `public-api.v1.delivery_notes.signature_audits.forget` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:gdpr_forget` — Erase personal data (GDPR Art. 17) from delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.signature_audits.forget GDPR Art. 17 (right to erasure) — remove the personal data (recipient name/DNI) from a signature audit log entry while preserving the non-PII audit trail required for LSSI-CE compliance. The `{auditId}` is the numeric primary key of the signature audit record. ## Path parameters - `auditId` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `id` (integer, required) - `forgotten` (boolean, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/delivery_notes/stats — Retrieve delivery note stats - **Operation ID**: `public-api.v1.delivery_notes.stats` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:read` — Read delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.stats Return aggregated KPIs for your delivery notes: total count, accumulated amount, per-status breakdown, count pending signature, and count converted to invoice this month. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNoteStats), required) — Resumen agregado de los albaranes de la empresa autenticada: total, importe acumulado, desglose por estado interno, pendientes de firma y convertidos a factura este mes. Devuelto por `GET /v1/delivery_notes/stats`. - `total_count` (integer, required) — Total number of recorded delivery notes. - `total_amount` (number, required) — Aggregate amount of the delivery notes (EUR). - `by_status` (object, required) — Breakdown by internal delivery note state. The key is the internal state (`draft`, `delivered`, `invoiced`, `cancelled`); the value is an object with `status`, `count` and `total` (accumulated amount in EUR). States with no delivery notes do not appear. - `pending_signature` (integer, required) — Delivered delivery notes (`delivered`) that do not yet have a recorded signature. - `converted_this_month` (integer, required) — Delivery notes converted to an invoice (`invoiced`) during the current month. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/delivery_notes/statuses — List delivery note statuses - **Operation ID**: `public-api.v1.delivery_notes.statuses` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:read` — Read delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.statuses List the closed catalog of delivery note statuses (`draft`, `delivered`, `invoiced`, `cancelled`) with their public labels and colors. Use it to populate filters or status pickers instead of hard-coding values. The response `data` is an array of `{ value, label, color }` items. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `value` (string, required, enum: `draft`, `delivered`, `invoiced`, `cancelled`) — Internal status identifier. - `label` (string, required) — Human-readable status label (Spanish). - `color` (string, required) — Suggested color for rendering the status in the UI. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/delivery_notes/{delivery_note} — Update a delivery note - **Operation ID**: `public-api.v1.delivery_notes.update` - **Tag**: Delivery Notes - **Required scope**: `delivery_notes:write` — Create and update delivery notes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/delivery-notes/public-api.v1.delivery_notes.update Update a draft delivery note. Once signed or invoiced, the delivery note becomes immutable. ## Path parameters - `delivery_note` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 24 properties; none of them required. Public REST API v1 — PUT /v1/delivery_notes/{uuid}. Partial update: omitted fields are kept. Only allowed when the delivery note is in `draft` status (the controller maps the transition exception to 422 `invalid_status_transition`). - `client_id` (string, optional, format: uuid) - `delivery_date` (string | null, optional, format: date) - `notes` (string | null, optional, maxLength 2000) - `internal_notes` (string | null, optional, maxLength 2000) - `reference_number` (string | null, optional, maxLength 255) - `transport_details` (string | null, optional, maxLength 2000) - `delivery_address` (string | null, optional, maxLength 500) - `delivery_city` (string | null, optional, maxLength 255) - `delivery_postal_code` (string | null, optional, maxLength 20) - `delivery_province` (string | null, optional, maxLength 255) - `delivery_country` (string | null, optional, maxLength 255) - `vehicle_plate` (string | null, optional, maxLength 20) — Licence plate of the vehicle used for the delivery (up to 20 characters). - `driver_name` (string | null, optional, maxLength 120) - `driver_tax_id` (string | null, optional, maxLength 20) - `tracking_number` (string | null, optional, maxLength 100) - `carrier_company` (string | null, optional, maxLength 120) - `received_by_name` (string | null, optional, maxLength 120) - `received_by_tax_id` (string | null, optional, maxLength 20) - `external_id` (string | null, optional, maxLength 100) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `billing_emails` (array | null, optional, maxItems 5) - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional, min 0.01) - `unit_price` (number, optional, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **200** - Body (`application/json`): - `data` (object (DeliveryNote), required) — A delivery note tracking goods delivered to a customer. - `id` (string, required) - `object` (string, required, enum: `delivery_note`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required, enum: `draft`, `sent`, `signed`, `invoiced`, `cancelled`) — Delivery note lifecycle status exposed by the public API: `draft` (created, editable), `sent` (goods marked as delivered, no signature recorded yet), `signed` (delivered AND a recipient signature has been recorded), `invoiced` (converted into an invoice, terminal), `cancelled` (terminal). Note: the internal `delivered` state is surfaced as `sent` (without signature) or `signed` (with signature) — there is no `delivered` value in the public API. Recording a signature on a `sent` note moves it to `signed` (it does not introduce a brand-new lifecycle stage; the signature presence is the only difference). - `issued_on` (string | null, required, format: date) - `delivery_date` (string | null, required, format: date) - `signed_at` (string | null, required, format: date-time) - `signed_by` (string | null, required) - `signature_image_url` (string | null, required, format: uri) - `vehicle_plate` (string | null, required) — License plate of the delivery vehicle. - `driver` (object | null, required) — Delivery driver data. `null` when no sub-field is provided. - `tracking` (object | null, required) — Shipment tracking data. `null` when no sub-field is provided. - `received_by` (object | null, required) — Goods recipient data. `null` when no sub-field is provided. - `billing_emails` (array, required, maxItems 5) — Additional emails for delivery note dispatch (administration, accounting). Maximum 5. Empty `[]` when there are none. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this delivery note was converted into. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The delivery note request conflicts with its current state — e.g. an invalid status transition (signing an already-signed delivery note), an attempt to sign a non-delivered note, or a reused idempotency key. - **422** — Validation failed, or the delivery note cannot undergo the requested state transition (e.g. signing a non-delivered note). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/developers/request-logs — List your API request logs - **Operation ID**: `public-api.v1.developers.request_logs.list` - **Tag**: Developers - **Required scope**: `developers:read` — Read developers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/developers/public-api.v1.developers.request_logs.list Inspect the requests your own integration has made against this API, newest first, so you can debug it without opening a support ticket: what you called, what came back, how long it took and, when a call failed, the error it returned. Scoped to the authenticated company. Rows are purged after 30 days, so this is a debugging window, not an audit trail. ## Query parameters - `method[]` (array, optional, enum: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`) — HTTP methods to filter by, either comma-separated (`?method=GET,POST`) or repeated (`?method[]=GET&method[]=POST`). - `status_range[]` (array, optional, enum: `2xx`, `3xx`, `4xx`, `5xx`) — Status code ranges to filter by, either comma-separated (`?status_range=4xx,5xx`) or repeated (`?status_range[]=4xx&status_range[]=5xx`). - `only_errors` (boolean, optional) — Shortcut for status_range=4xx,5xx. - `api_key_prefix` (string, optional, maxLength 64) — First characters of the API key identifier that issued the request. - `path_search` (string, optional, maxLength 255) — Partial, case-insensitive match against the request path. - `created_at[gte]` (string, optional, format: date-time) — Inclusive lower bound of created_at (ISO 8601). - `created_at[lte]` (string, optional, format: date-time) — Inclusive upper bound of created_at (ISO 8601). - `environment` (string, optional, enum: `live`, `test`) — Environment of the API key that issued the request (live, test). - `limit` (integer, optional, min 1, max 100, default: `50`) — Number of logs to return. - `starting_after` (string, optional, pattern: `^[0-9]+$`) — Cursor for forward pagination: pass back the `next_cursor` of the previous page. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `ApiRequestLogListV1Resource` - Body (`application/json`): - `data` (array, required) — Page of request logs, newest first. - `request_id` (string, required) — Opaque identifier of the request (`req_` + ULID), the same value returned in the `X-Request-Id` response header of that call. It is the identity of this resource — quote it in support requests and use it in `GET /v1/developers/request-logs/{request_id}`. Deliberately NOT a UUID v7: request logs are ephemeral rows, not domain resources. - `object` (string, required, enum: `api_request_log`) — Always `api_request_log`. - `api_key_prefix` (string | null, required) — First 8 characters of the identifier of the API key that authenticated the request — enough to attribute the call to one of your keys without exposing the full identifier. `null` when the request was not authenticated. The plaintext secret is never involved. - `method` (string, required, enum: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`) — HTTP method of the request. - `path` (string, required) — Request path, without the query string (which is never stored). - `status_code` (integer, required) — HTTP status code of the response. - `duration_ms` (integer, required) — Server-side processing time in milliseconds. - `ip` (string, required) — Source IP address of the request. - `user_agent` (string | null, required) — `User-Agent` header sent by the client, or `null` when absent. - `error_type` (string | null, required) — Error family of the response envelope (e.g. `invalid_request_error`, `not_found_error`). `null` for 2xx and 3xx responses. - `error_code` (string | null, required) — Machine-readable error code of the response envelope (e.g. `invalid_param_value`, `resource_not_found`). `null` for 2xx and 3xx responses. - `factuarea_version` (string | null, required) — API version (date-based) the request was resolved under, as reported in the `Factuarea-Version` response header. `null` for requests logged before versions were sealed. - `environment` (string, required, enum: `live`, `test`) — Environment of the key that issued the request: `live` (`fact_live_`) or `test` (`fact_test_`). Test keys operate on the sandbox company, so their logs belong to a different tenant and a live key never sees them; the filter is what lets a test key inspect its own traffic. - `created_at` (string, required, format: date-time) — When the request was received (ISO 8601). - `has_more` (boolean, required) — `true` when more logs exist beyond this page. - `next_cursor` (string | null, required) — Opaque cursor to pass as `starting_after` for the next page, or `null` when `has_more` is `false`. Unlike the rest of the v1 listings it is a numeric string, not a UUID v7 — treat it as opaque and reuse it verbatim. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/developers/request-logs/{request_id} — Retrieve an API request log - **Operation ID**: `public-api.v1.developers.request_logs.show` - **Tag**: Developers - **Required scope**: `developers:read` — Read developers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/developers/public-api.v1.developers.request_logs.show Retrieve a single request of your own integration by the `request_id` the API returned in the `X-Request-Id` header of that response — the identifier you already have in hand when a call misbehaved, and the one to quote in a support request. It is an opaque `req_…` string, not a UUID v7. The body carries the same fields as the listing. ## Path parameters - `request_id` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ApiRequestLog), required) — A single request your integration made against this API. Read-only, scoped to your own company, and retained for 30 days before being purged. Request headers, bodies and query strings are never stored, so they are never returned. - `request_id` (string, required) — Opaque identifier of the request (`req_` + ULID), the same value returned in the `X-Request-Id` response header of that call. It is the identity of this resource — quote it in support requests and use it in `GET /v1/developers/request-logs/{request_id}`. Deliberately NOT a UUID v7: request logs are ephemeral rows, not domain resources. - `object` (string, required, enum: `api_request_log`) — Always `api_request_log`. - `api_key_prefix` (string | null, required) — First 8 characters of the identifier of the API key that authenticated the request — enough to attribute the call to one of your keys without exposing the full identifier. `null` when the request was not authenticated. The plaintext secret is never involved. - `method` (string, required, enum: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`) — HTTP method of the request. - `path` (string, required) — Request path, without the query string (which is never stored). - `status_code` (integer, required) — HTTP status code of the response. - `duration_ms` (integer, required) — Server-side processing time in milliseconds. - `ip` (string, required) — Source IP address of the request. - `user_agent` (string | null, required) — `User-Agent` header sent by the client, or `null` when absent. - `error_type` (string | null, required) — Error family of the response envelope (e.g. `invalid_request_error`, `not_found_error`). `null` for 2xx and 3xx responses. - `error_code` (string | null, required) — Machine-readable error code of the response envelope (e.g. `invalid_param_value`, `resource_not_found`). `null` for 2xx and 3xx responses. - `factuarea_version` (string | null, required) — API version (date-based) the request was resolved under, as reported in the `Factuarea-Version` response header. `null` for requests logged before versions were sealed. - `environment` (string, required, enum: `live`, `test`) — Environment of the key that issued the request: `live` (`fact_live_`) or `test` (`fact_test_`). Test keys operate on the sandbox company, so their logs belong to a different tenant and a live key never sees them; the filter is what lets a test key inspect its own traffic. - `created_at` (string, required, format: date-time) — When the request was received (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/emails/indicators — Summarize email delivery per document - **Operation ID**: `public-api.v1.emails.indicators` - **Tag**: Emails - **Required scope**: `emails:read` — Read emails. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/emails/public-api.v1.emails.indicators Answer "did the email for these documents go out?" for a whole batch at once, instead of paging through the deliveries of each one: per document, how many emails were sent, the last status, the last hand-off and how many failed. Ideal to paint a "sent / not sent" column over a page of invoices in a single call. IMPORTANT — `last_status` and `last_sent_at` describe the hand-off to the OUTGOING SMTP SERVER, not real delivery: a `sent` email may still bounce afterwards without the platform observing it. ## Query parameters - `related_entity_ids[]` (array, required, format: uuid) — Documents to summarize: UUID v7 of each one, either comma-separated (`related_entity_ids=a,b,c`) or repeated. - `related_entity_type` (string, optional, enum: `invoice`, `quote`, `proforma`, `delivery_note`, `purchase_invoice`, `recurring_invoice`) — Optional document type that narrows the resolution of every id in the batch. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `EmailDeliveryIndicatorsV1Resource` - Body (`application/json`): - `data` (array, required) — One entry per requested document that has at least one email. - `related_entity_id` (string, required, format: uuid) — UUID v7 of the document, exactly as you sent it in `related_entity_ids`. Match the results back by this field, never by position. - `related_entity_type` (string, required, enum: `invoice`, `quote`, `proforma`, `delivery_note`, `purchase_invoice`, `recurring_invoice`) — Document type RESOLVED for this id. It is always reported, even when you did not send `related_entity_type`, so a batch can mix types safely. - `object` (string, required, enum: `email_delivery_indicator`) — Always `email_delivery_indicator`. - `total` (integer, required) — Number of emails registered for this document. - `last_status` (string, required, enum: `queued`, `sending`, `sent`, `failed`) — Status of the most recent email of this document. Status of the send. It describes the hand-off to the OUTGOING SMTP SERVER, NOT real delivery to the recipient's mailbox: `queued` (accepted, waiting for the worker), `sending` (being handed off), `sent` (the outgoing mail server accepted the message) and `failed` (every attempt failed). A `sent` message can still bounce or land in spam afterwards without the platform finding out. There are deliberately no `delivered`, `bounced` or `opened` values: observing them would require a mail provider with delivery webhooks, which is out of scope. - `last_sent_at` (string | null, required, format: date-time) — Latest time something left for the outgoing mail server (ISO 8601), or `null` if nothing ever did. It is the MAXIMUM across the document's emails, so it can be non-null while `last_status` is `failed`: the last attempt failed, an earlier one went out. - `attempts` (integer, required) — Highest number of attempts among the emails of this document — the maximum, NOT their sum. - `failed_count` (integer, required) — How many emails of this document ended in `failed`. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/emails — List sent emails - **Operation ID**: `public-api.v1.emails.list` - **Tag**: Emails - **Required scope**: `emails:read` — Read emails. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/emails/public-api.v1.emails.list Browse the emails your company has sent through the platform — invoices, quotes, pro formas, delivery notes, payment reminders — newest first, so you can answer "did the email for this invoice actually go out?" without asking your customer. Scoped to the authenticated company. IMPORTANT — `status` describes the hand-off to the OUTGOING SMTP SERVER, not real delivery: `sent` means the outgoing mail server accepted the message, not that the recipient received it. ## Query parameters - `status` (string, optional, enum: `queued`, `sending`, `sent`, `failed`) — Delivery status to filter by (queued, sending, sent, failed). - `recipient_email` (string, optional, maxLength 255) — Exact recipient address to filter by. - `related_entity_id` (string, optional, format: uuid) — UUID v7 of the related document. - `related_entity_type` (string, optional, enum: `invoice`, `quote`, `proforma`, `delivery_note`, `purchase_invoice`, `recurring_invoice`) — Optional document type that narrows the resolution of related_entity_id. - `created_at[gte]` (string, optional, format: date-time) — Inclusive lower bound of created_at (ISO 8601). - `created_at[lte]` (string, optional, format: date-time) — Inclusive upper bound of created_at (ISO 8601). - `search` (string, optional, maxLength 255) — Partial, case-insensitive match against the subject and the recipient. - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of emails to return. - `starting_after` (string, optional, pattern: `^[0-9]+$`) — Cursor for forward pagination: pass back the `next_cursor` of the previous page. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `EmailDeliveryListV1Resource` - Body (`application/json`): - `data` (array, required) — Page of sent emails, newest first. - `id` (string, required, format: uuid) — UUID v7 of the send. Use it in `GET /v1/emails/{email}`. - `object` (string, required, enum: `email_delivery`) — Always `email_delivery`. - `recipient_email` (string, required, format: email) — Address the message was sent to. - `cc` (array, required) — Addresses in copy. Empty array when there were none. - `bcc` (array, required) — Addresses in blind copy. Empty array when there were none. - `subject` (string, required) — Subject line of the message. - `status` (string, required, enum: `queued`, `sending`, `sent`, `failed`) — Status of the send. It describes the hand-off to the OUTGOING SMTP SERVER, NOT real delivery to the recipient's mailbox: `queued` (accepted, waiting for the worker), `sending` (being handed off), `sent` (the outgoing mail server accepted the message) and `failed` (every attempt failed). A `sent` message can still bounce or land in spam afterwards without the platform finding out. There are deliberately no `delivered`, `bounced` or `opened` values: observing them would require a mail provider with delivery webhooks, which is out of scope. - `attempts` (integer, required) — Number of hand-off attempts made for this message. - `error_message` (string | null, required) — Error reported by the outgoing mail server on the last failed attempt, or `null` when there was none. - `sent_at` (string | null, required, format: date-time) — When the outgoing mail server accepted the message (ISO 8601), or `null` if it never did. It marks the hand-off, not a confirmed delivery or a read. - `failed_at` (string | null, required, format: date-time) — When the last attempt failed (ISO 8601), or `null` when none failed. - `created_at` (string | null, required, format: date-time) — When the send was registered (ISO 8601). - `related_entity_type` (string | null, required, enum: `invoice`, `quote`, `proforma`, `delivery_note`, `purchase_invoice`, `recurring_invoice`, `null`) — Type of the document the email was sent for, or `null` for non-document emails (test messages, account emails). - `related_entity_id` (string | null, required, format: uuid) — UUID v7 of the related document, or `null` for non-document emails and for older rows recorded before the sender stored it. - `has_more` (boolean, required) — `true` when more emails exist beyond this page. - `next_cursor` (string | null, required) — Opaque cursor to pass as `starting_after` for the next page, or `null` when `has_more` is `false`. Unlike the rest of the v1 listings it is a numeric string, not a UUID v7 — treat it as opaque and reuse it verbatim. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/emails/{email} — Retrieve a sent email - **Operation ID**: `public-api.v1.emails.show` - **Tag**: Emails - **Required scope**: `emails:read` — Read emails. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/emails/public-api.v1.emails.show Retrieve one email by its id, typically after finding it in the listing, to investigate what happened to it: recipient, subject, status, attempts, the error message when it failed, and the document it was sent for. Scoped to the authenticated company. IMPORTANT — `status` describes the hand-off to the OUTGOING SMTP SERVER, not real delivery: `sent` means the outgoing mail server accepted the message, not that the recipient received it. ## Path parameters - `email` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmailDelivery), required) — One email your company sent through the platform (an invoice, a quote, a payment reminder…). Read-only and scoped to your own company. Internal implementation details — the mailable class, the queue job id, the user id and the raw metadata — are never exposed. - `id` (string, required, format: uuid) — UUID v7 of the send. Use it in `GET /v1/emails/{email}`. - `object` (string, required, enum: `email_delivery`) — Always `email_delivery`. - `recipient_email` (string, required, format: email) — Address the message was sent to. - `cc` (array, required) — Addresses in copy. Empty array when there were none. - `bcc` (array, required) — Addresses in blind copy. Empty array when there were none. - `subject` (string, required) — Subject line of the message. - `status` (string, required, enum: `queued`, `sending`, `sent`, `failed`) — Status of the send. It describes the hand-off to the OUTGOING SMTP SERVER, NOT real delivery to the recipient's mailbox: `queued` (accepted, waiting for the worker), `sending` (being handed off), `sent` (the outgoing mail server accepted the message) and `failed` (every attempt failed). A `sent` message can still bounce or land in spam afterwards without the platform finding out. There are deliberately no `delivered`, `bounced` or `opened` values: observing them would require a mail provider with delivery webhooks, which is out of scope. - `attempts` (integer, required) — Number of hand-off attempts made for this message. - `error_message` (string | null, required) — Error reported by the outgoing mail server on the last failed attempt, or `null` when there was none. - `sent_at` (string | null, required, format: date-time) — When the outgoing mail server accepted the message (ISO 8601), or `null` if it never did. It marks the hand-off, not a confirmed delivery or a read. - `failed_at` (string | null, required, format: date-time) — When the last attempt failed (ISO 8601), or `null` when none failed. - `created_at` (string | null, required, format: date-time) — When the send was registered (ISO 8601). - `related_entity_type` (string | null, required, enum: `invoice`, `quote`, `proforma`, `delivery_note`, `purchase_invoice`, `recurring_invoice`, `null`) — Type of the document the email was sent for, or `null` for non-document emails (test messages, account emails). - `related_entity_id` (string | null, required, format: uuid) — UUID v7 of the related document, or `null` for non-document emails and for older rows recorded before the sender stored it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/employee-invitations/{invitation} — Cancel an employee invitation - **Operation ID**: `public-api.v1.employee-invitations.cancel` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-invitations.cancel Cancel a pending employee invitation identified by its `id` (UUID v7); it transitions to `canceled` and can no longer be accepted. Returns 204 on success, 422 if the invitation was already accepted, and 404 if it does not exist in your company. ## Path parameters - `invitation` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/employee-invitations — List employee invitations - **Operation ID**: `public-api.v1.employee-invitations.list` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-invitations.list List the employee invitations of your company. Only invitations with role `employee` are returned; user/admin invitations from the user-management surface are excluded. Each item exposes its opaque `id` (UUID v7), `email`, `status` (`pending`/`accepted`/`canceled`/`expired`) and expiry. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the invitation, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee_invitation`) — Always `employee_invitation`. - `email` (string, required, format: email) — Email address the invitation was sent to. - `role` (string, required, enum: `employee`) — Always `employee` for this surface. - `status` (string, required, enum: `pending`, `accepted`, `canceled`, `expired`) — Lifecycle status of the invitation: `pending` (awaiting acceptance), `accepted`, `canceled` or `expired`. - `expires_at` (string | null, required, format: date-time) — Expiry timestamp of the invitation link (ISO 8601), or `null`. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employee-invitations/{invitation}/resend — Resend an employee invitation - **Operation ID**: `public-api.v1.employee-invitations.resend` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-invitations.resend Resend a pending employee invitation identified by its `id` (UUID v7), regenerating its token and expiry and re-sending the invitation email. Returns 422 if the invitation was already accepted or canceled, and 404 if it does not exist in your company. ## Path parameters - `invitation` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeInvitation), required) — An invitation for a person to join your company as an employee (Control Horario portal). Reuses the invitation subsystem with role `employee`; it does not consume the plan `users` seat limit. - `id` (string, required, format: uuid) — Opaque identifier of the invitation, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee_invitation`) — Always `employee_invitation`. - `email` (string, required, format: email) — Email address the invitation was sent to. - `role` (string, required, enum: `employee`) — Always `employee` for this surface. - `status` (string, required, enum: `pending`, `accepted`, `canceled`, `expired`) — Lifecycle status of the invitation: `pending` (awaiting acceptance), `accepted`, `canceled` or `expired`. - `expires_at` (string | null, required, format: date-time) — Expiry timestamp of the invitation link (ISO 8601), or `null`. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employee-invitations — Send an employee invitation - **Operation ID**: `public-api.v1.employee-invitations.send` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-invitations.send Invite a person to join your company as an employee (Control Horario portal). Only `email` is required — the `employee` role is fixed by the server, never taken from the payload. The invited person receives an email with an acceptance link. Inviting an email that already belongs to a company user, or one that already has a pending invitation, returns 422. Employee invitations do not consume the plan `users` seat limit. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `email`. - `email` (string, required, format: email, maxLength 255) — Email of the employee to invite (the `employee` role is set by the server). ## Responses - **201** - Body (`application/json`): - `data` (object (EmployeeInvitation), required) — An invitation for a person to join your company as an employee (Control Horario portal). Reuses the invitation subsystem with role `employee`; it does not consume the plan `users` seat limit. - `id` (string, required, format: uuid) — Opaque identifier of the invitation, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee_invitation`) — Always `employee_invitation`. - `email` (string, required, format: email) — Email address the invitation was sent to. - `role` (string, required, enum: `employee`) — Always `employee` for this surface. - `status` (string, required, enum: `pending`, `accepted`, `canceled`, `expired`) — Lifecycle status of the invitation: `pending` (awaiting acceptance), `accepted`, `canceled` or `expired`. - `expires_at` (string | null, required, format: date-time) — Expiry timestamp of the invitation link (ISO 8601), or `null`. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employee-seats/cancel — Cancel the employee seat add-on - **Operation ID**: `public-api.v1.employee-seats.cancel` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-seats.cancel Cancel the per-employee billing add-on: the `employee-seats` subscription is cancelled at period end (the current month is already paid) and the per-employee coverage is purged. The plan subscription is never touched. Returns the resulting billing status, where `subscribed` stays `true` until the period ends. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeSeatBillingStatus), required) — Billing status of the per-employee add-on for your company (Control Horario). Whether the dedicated `employee-seats` subscription is active, how many seats are billed, how many employees are active and the recurring per-seat cost. Amounts are in minor currency units (cents) and are null when the cost is not resolvable (not subscribed, no active plan, enterprise outside Stripe, sandbox) — never a misleading 0. Employees never count towards the plan `users` seat limit. Read `subscribed` together with `seats_billable` to tell a legitimate no-charge state apart from a billing anomaly. - `object` (string, required, enum: `employee_seat_billing_status`) — Stripe-like discriminator. Always `employee_seat_billing_status` for this resource. - `subscribed` (boolean, required) — true when the company has the per-employee add-on subscribed (`employee-seats` subscription active). - `quantity` (integer, required) — Number of BILLED seats: while subscribed it follows the number of active employees; 0 when not subscribed. - `active_employees` (integer, required) — Real number of `active` employees of the company. When not subscribed it reveals how many seats an opt-in would bill; when subscribed it matches `quantity`. - `unit_amount` (integer | null, required) — Taxable base of one seat per billing cycle, in cents, or null when not resolvable. - `tax_rate` (integer | null, required) — Applied VAT rate (e.g. 21), or null when not resolved. - `tax_amount` (integer | null, required) — VAT amount of one seat, in cents, or null. - `total_per_seat` (integer | null, required) — Base + VAT of one seat, in cents, or null. - `recurring_total` (integer | null, required) — Projected recurring total (`total_per_seat` × `quantity`), in cents. Null when VAT is not resolved. - `currency` (string, required) — ISO 4217 currency code (uppercase). - `billing_interval` (string | null, required, enum: `month`, `null`) — The seat is always billed monthly, regardless of the plan interval; null when not subscribed. - `next_invoice_at` (string | null, required, format: date-time) — End of the current covered period of the seat subscription (ISO 8601), or null. - `requires_active_plan` (boolean, required) — true when the company has no active paid plan, so the seat cost cannot be resolved. - `seats_billable` (boolean, required) — true when your company MUST be paying per seat (live paid plan), regardless of whether the `employee-seats` subscription exists — it is independent of `subscribed`. Combine both: `subscribed: true` means seats are being billed normally; `subscribed: false` with `seats_billable: true` and `active_employees > 0` is a BILLING ANOMALY (active employees with no seat billed) that your provider must settle; `seats_billable: false` is a legitimate no-charge state (enterprise by contract, trial or sandbox). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employee-seats/change-quantity — Sync the employee seat quantity - **Operation ID**: `public-api.v1.employee-seats.change-quantity` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-seats.change-quantity Reconcile the seat quantity of the add-on to the real number of active employees (SET with `proration_behavior: none`, no invoice). Idempotent: when the quantity already matches it is a no-op. Returns the resulting billing status. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeSeatBillingStatus), required) — Billing status of the per-employee add-on for your company (Control Horario). Whether the dedicated `employee-seats` subscription is active, how many seats are billed, how many employees are active and the recurring per-seat cost. Amounts are in minor currency units (cents) and are null when the cost is not resolvable (not subscribed, no active plan, enterprise outside Stripe, sandbox) — never a misleading 0. Employees never count towards the plan `users` seat limit. Read `subscribed` together with `seats_billable` to tell a legitimate no-charge state apart from a billing anomaly. - `object` (string, required, enum: `employee_seat_billing_status`) — Stripe-like discriminator. Always `employee_seat_billing_status` for this resource. - `subscribed` (boolean, required) — true when the company has the per-employee add-on subscribed (`employee-seats` subscription active). - `quantity` (integer, required) — Number of BILLED seats: while subscribed it follows the number of active employees; 0 when not subscribed. - `active_employees` (integer, required) — Real number of `active` employees of the company. When not subscribed it reveals how many seats an opt-in would bill; when subscribed it matches `quantity`. - `unit_amount` (integer | null, required) — Taxable base of one seat per billing cycle, in cents, or null when not resolvable. - `tax_rate` (integer | null, required) — Applied VAT rate (e.g. 21), or null when not resolved. - `tax_amount` (integer | null, required) — VAT amount of one seat, in cents, or null. - `total_per_seat` (integer | null, required) — Base + VAT of one seat, in cents, or null. - `recurring_total` (integer | null, required) — Projected recurring total (`total_per_seat` × `quantity`), in cents. Null when VAT is not resolved. - `currency` (string, required) — ISO 4217 currency code (uppercase). - `billing_interval` (string | null, required, enum: `month`, `null`) — The seat is always billed monthly, regardless of the plan interval; null when not subscribed. - `next_invoice_at` (string | null, required, format: date-time) — End of the current covered period of the seat subscription (ISO 8601), or null. - `requires_active_plan` (boolean, required) — true when the company has no active paid plan, so the seat cost cannot be resolved. - `seats_billable` (boolean, required) — true when your company MUST be paying per seat (live paid plan), regardless of whether the `employee-seats` subscription exists — it is independent of `subscribed`. Combine both: `subscribed: true` means seats are being billed normally; `subscribed: false` with `seats_billable: true` and `active_employees > 0` is a BILLING ANOMALY (active employees with no seat billed) that your provider must settle; `seats_billable: false` is a legitimate no-charge state (enterprise by contract, trial or sandbox). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/employee-seats/preview — Preview the employee seat charge - **Operation ID**: `public-api.v1.employee-seats.preview` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-seats.preview Preview the prorated per-seat amount for activating or hiring employees, computed from the Stripe upcoming invoice of the `employee-seats` subscription, without charging. Use `count` (≥1, up to 1000) for a batch preview, or `employee_ids` (UUID v7) for a coverage-aware preview: employees still covered for the current period cost 0 (`already_covered: true`). `amount` is the taxable base in cents; `requires_payment_method` is `true` when no payment method is on file. Never throws — it degrades to a neutral preview. ## Query parameters - `count` (integer | null, optional, min 1, max 1000) — Number of employees that would be activated in bulk (at least 1, default 1). - `employee_ids[]` (array, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeSeatChargePreview), required) — Preview of the prorated per-seat charge for activating or hiring employees, computed from the Stripe upcoming invoice without charging. The exclusive reason flags for a 0 amount, by precedence: `already_covered` > `requires_active_plan` > `included_in_trial`. At most one is true. `requires_payment_method` is NOT a 0-amount branch: it flags an obstacle on a REAL amount. Read `seats_billable` before promising a charge: when false the operation never bills (enterprise by contract, trial, sandbox or live mode off) even though the amount is computed. Amounts are in minor currency units (cents). - `object` (string, required, enum: `employee_seat_charge_preview`) — Stripe-like discriminator. Always `employee_seat_charge_preview` for this resource. - `amount` (integer, required) — Taxable base of the proration (without VAT), in cents. - `tax_amount` (integer, required) — VAT amount of the proration, in cents. - `total` (integer, required) — Total actually charged (`amount + tax_amount`), in cents. - `tax_rate` (integer | null, required) — Applied VAT rate (e.g. 21), or null when Stripe Tax did not resolve it (then `tax_amount` is 0 and `total` = `amount`). - `currency` (string, required) — ISO 4217 currency code (uppercase). - `next_invoice_date` (string | null, required, format: date-time) — End of the current period of the seat subscription (ISO 8601), never today; null when not resolvable. - `requires_payment_method` (boolean, required) — true when the company operates in live mode with no payment method on file. This does NOT zero the amount: the proration is still computed (previewing it needs no card), so you can show what the operation will cost before asking for the card. Configure the payment method to proceed. - `requires_active_plan` (boolean, required) — true when the company has no active paid plan (amount is 0). - `included_in_trial` (boolean, required) — true when the company is on trial, so the activation is free (amount is 0) until the trial converts to a paid plan. - `already_covered` (boolean, required) — true when every employee to activate is still covered this period (free reactivation, amount is 0). - `is_first_seat` (boolean, required) — true when the activation creates the FIRST seat subscription of the company (full-month charge that anchors the monthly billing day). - `recurring_quantity` (integer | null, required) — Projected total seat quantity (current + new) of the joint monthly cost after activation, or null on reason branches. - `recurring_base_cents` (integer | null, required) — Base (without VAT) of the projected joint monthly cost, in cents, or null. - `recurring_total_cents` (integer | null, required) — Projected joint monthly cost with VAT, in cents; null when VAT is not calculable. - `seats_billable` (boolean, required) — true when the seat WILL actually be billed: the very same predicate the hiring/reactivation gate uses (live paid plan, company not in sandbox) and the same field `GET /employee-seats` exposes. When false the operation never charges (enterprise by contract, trial, sandbox or live mode off) even though `amount` is computed — do not present it as a payment, and do not ask for confirmation of a charge that will not happen. Independent of the 0-amount branches: it can be true together with `already_covered` (the plan does bill seats, but THIS operation is free). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/employee-seats — Retrieve employee seat billing status - **Operation ID**: `public-api.v1.employee-seats.status` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-seats.status Return the billing status of the per-employee add-on for your company: whether the `employee-seats` subscription is active, how many seats are billed (`quantity`), how many employees are active, and the recurring per-seat cost with VAT. Amounts are in the currency minor units (cents) and are `null` when the cost is not resolvable (not subscribed, no active plan, enterprise outside Stripe, sandbox) — never a misleading 0. `seats_billable` tells whether your plan MUST be paying per seat, independently of `subscribed`: `subscribed: false` with `seats_billable: true` and active employees is a billing anomaly, while `seats_billable: false` is a legitimate no-charge state (enterprise by contract, trial or sandbox). Employees never count towards the plan `users` seat limit. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeSeatBillingStatus), required) — Billing status of the per-employee add-on for your company (Control Horario). Whether the dedicated `employee-seats` subscription is active, how many seats are billed, how many employees are active and the recurring per-seat cost. Amounts are in minor currency units (cents) and are null when the cost is not resolvable (not subscribed, no active plan, enterprise outside Stripe, sandbox) — never a misleading 0. Employees never count towards the plan `users` seat limit. Read `subscribed` together with `seats_billable` to tell a legitimate no-charge state apart from a billing anomaly. - `object` (string, required, enum: `employee_seat_billing_status`) — Stripe-like discriminator. Always `employee_seat_billing_status` for this resource. - `subscribed` (boolean, required) — true when the company has the per-employee add-on subscribed (`employee-seats` subscription active). - `quantity` (integer, required) — Number of BILLED seats: while subscribed it follows the number of active employees; 0 when not subscribed. - `active_employees` (integer, required) — Real number of `active` employees of the company. When not subscribed it reveals how many seats an opt-in would bill; when subscribed it matches `quantity`. - `unit_amount` (integer | null, required) — Taxable base of one seat per billing cycle, in cents, or null when not resolvable. - `tax_rate` (integer | null, required) — Applied VAT rate (e.g. 21), or null when not resolved. - `tax_amount` (integer | null, required) — VAT amount of one seat, in cents, or null. - `total_per_seat` (integer | null, required) — Base + VAT of one seat, in cents, or null. - `recurring_total` (integer | null, required) — Projected recurring total (`total_per_seat` × `quantity`), in cents. Null when VAT is not resolved. - `currency` (string, required) — ISO 4217 currency code (uppercase). - `billing_interval` (string | null, required, enum: `month`, `null`) — The seat is always billed monthly, regardless of the plan interval; null when not subscribed. - `next_invoice_at` (string | null, required, format: date-time) — End of the current covered period of the seat subscription (ISO 8601), or null. - `requires_active_plan` (boolean, required) — true when the company has no active paid plan, so the seat cost cannot be resolved. - `seats_billable` (boolean, required) — true when your company MUST be paying per seat (live paid plan), regardless of whether the `employee-seats` subscription exists — it is independent of `subscribed`. Combine both: `subscribed: true` means seats are being billed normally; `subscribed: false` with `seats_billable: true` and `active_employees > 0` is a BILLING ANOMALY (active employees with no seat billed) that your provider must settle; `seats_billable: false` is a legitimate no-charge state (enterprise by contract, trial or sandbox). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employee-seats/subscribe — Subscribe to the employee seat add-on - **Operation ID**: `public-api.v1.employee-seats.subscribe` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employee-seats.subscribe Opt in to the per-employee billing add-on: create the dedicated monthly `employee-seats` subscription with `quantity` set to the number of active employees, charging the first period with the payment method on file. The charge is atomic — with no payment method it returns 402 `employee_seat_payment_method_required` (the envelope carries `error.details.payment_setup_url`), and a declined charge returns 402 `employee_seat_charge_failed`; in both cases nothing is subscribed. Returns the resulting billing status. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeSeatBillingStatus), required) — Billing status of the per-employee add-on for your company (Control Horario). Whether the dedicated `employee-seats` subscription is active, how many seats are billed, how many employees are active and the recurring per-seat cost. Amounts are in minor currency units (cents) and are null when the cost is not resolvable (not subscribed, no active plan, enterprise outside Stripe, sandbox) — never a misleading 0. Employees never count towards the plan `users` seat limit. Read `subscribed` together with `seats_billable` to tell a legitimate no-charge state apart from a billing anomaly. - `object` (string, required, enum: `employee_seat_billing_status`) — Stripe-like discriminator. Always `employee_seat_billing_status` for this resource. - `subscribed` (boolean, required) — true when the company has the per-employee add-on subscribed (`employee-seats` subscription active). - `quantity` (integer, required) — Number of BILLED seats: while subscribed it follows the number of active employees; 0 when not subscribed. - `active_employees` (integer, required) — Real number of `active` employees of the company. When not subscribed it reveals how many seats an opt-in would bill; when subscribed it matches `quantity`. - `unit_amount` (integer | null, required) — Taxable base of one seat per billing cycle, in cents, or null when not resolvable. - `tax_rate` (integer | null, required) — Applied VAT rate (e.g. 21), or null when not resolved. - `tax_amount` (integer | null, required) — VAT amount of one seat, in cents, or null. - `total_per_seat` (integer | null, required) — Base + VAT of one seat, in cents, or null. - `recurring_total` (integer | null, required) — Projected recurring total (`total_per_seat` × `quantity`), in cents. Null when VAT is not resolved. - `currency` (string, required) — ISO 4217 currency code (uppercase). - `billing_interval` (string | null, required, enum: `month`, `null`) — The seat is always billed monthly, regardless of the plan interval; null when not subscribed. - `next_invoice_at` (string | null, required, format: date-time) — End of the current covered period of the seat subscription (ISO 8601), or null. - `requires_active_plan` (boolean, required) — true when the company has no active paid plan, so the seat cost cannot be resolved. - `seats_billable` (boolean, required) — true when your company MUST be paying per seat (live paid plan), regardless of whether the `employee-seats` subscription exists — it is independent of `subscribed`. Combine both: `subscribed: true` means seats are being billed normally; `subscribed: false` with `seats_billable: true` and `active_employees > 0` is a BILLING ANOMALY (active employees with no seat billed) that your provider must settle; `seats_billable: false` is a legitimate no-charge state (enterprise by contract, trial or sandbox). - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employees — Create an employee - **Operation ID**: `public-api.v1.employees.create` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.create Register a new employee for the authenticated company (resolved from the API key, never from the payload). `first_name`, `last_name`, `email`, `employment_type` (`full_time`/`part_time`), `contract_hours`, `hire_date` and `ccaa` are required; `tax_id` and `job_title` are optional. Returns the created employee with its generated `id` (UUID v7). Active employees count towards the workforce module seat billing. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 11 properties; 7 required: `first_name`, `last_name`, `email`, `employment_type`, `contract_hours`, `hire_date`, `ccaa`. - `first_name` (string, required, maxLength 100) — Employee first name. - `last_name` (string, required, maxLength 100) — Employee last name. - `email` (string, required, format: email, maxLength 255) — Employee email (unique within the company). - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required) — Weekly contracted hours (greater than 0 and up to 168). - `hire_date` (string, required, format: date) — Employee hire date, in `Y-m-d`. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community or city (ISO 3166-2:ES) used to localise public holidays. - `tax_id` (string | null, optional, maxLength 20) — Spanish fiscal identifier (NIF/NIE) of the employee (optional). - `job_title` (string | null, optional, maxLength 120) — Role or position of the employee (optional). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM), unique per company - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. ## Responses - **201** - Body (`application/json`): - `data` (object (Employee), required) — An employee of your company. Active employees count towards the seat billing of the workforce module; deactivated ones are preserved with their termination date. - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employees/{employee}/deactivate — Deactivate an employee - **Operation ID**: `public-api.v1.employees.deactivate` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.deactivate Deactivate an employee (transition `active` → `inactive`), soft-removing them from the active workforce while preserving their record. `termination_date` (`Y-m-d`) is optional — omit it to use today. Returns 422 if the employee is already inactive or the termination date precedes the hire date. Reversible via reactivate. ## Path parameters - `employee` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. - `termination_date` (string | null, optional, format: date) — Effective termination date, in `Y-m-d`. Defaults to today when omitted. ## Responses - **200** - Body (`application/json`): - `data` (object (Employee), required) — An employee of your company. Active employees count towards the seat billing of the workforce module; deactivated ones are preserved with their termination date. - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employees/find-by-external-id — Find an employee by external ID - **Operation ID**: `public-api.v1.employees.find_by_external_id` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.find_by_external_id Look up an employee by their `external_id` (sent in the JSON body), the integration key that maps them to a record in a third-party system (ERP/CRM/HR). Distinct from the fiscal `tax_id`. Returns the matching employee or 404 if no employee uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Employee), required) — An employee of your company. Active employees count towards the seat billing of the workforce module; deactivated ones are preserved with their termination date. - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/employees — List all employees - **Operation ID**: `public-api.v1.employees.list` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.list List the employees of your company with cursor-based pagination. Supports filtering by `status` (`active`/`inactive`), `employment_type` (`full_time`/`part_time`) and `ccaa`, plus free-text `search` over name and email. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional, enum: `active`, `inactive`) — Employment status of the employee. - `status[in]` (string, optional) — Employment status of the employee. - `employment_type` (string, optional, enum: `full_time`, `part_time`) — Type of employment contract. - `employment_type[in]` (string, optional) — Type of employment contract. - `ccaa` (string, optional) — Spanish autonomous community code (ISO 3166-2:ES) of the employee. - `ccaa[in]` (string, optional) — Spanish autonomous community code (ISO 3166-2:ES) of the employee. - `search` (string, optional, maxLength 80) — Free-text search. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/employees/{employee}/reactivate — Reactivate an employee - **Operation ID**: `public-api.v1.employees.reactivate` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.reactivate Reactivate an employee (transition `inactive` → `active`), clearing their `termination_date` and returning them to the active workforce. No request body. Returns 422 if the employee is already active. ## Path parameters - `employee` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Employee), required) — An employee of your company. Active employees count towards the seat billing of the workforce module; deactivated ones are preserved with their termination date. - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/employees/{employee} — Retrieve an employee - **Operation ID**: `public-api.v1.employees.show` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.show Retrieve a single employee by its `id` (UUID v7). An employee belonging to another company returns 404 `employee_not_found` (anti-enumeration). ## Path parameters - `employee` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Employee), required) — An employee of your company. Active employees count towards the seat billing of the workforce module; deactivated ones are preserved with their termination date. - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/employees/stats — Get employee stats - **Operation ID**: `public-api.v1.employees.stats` - **Tag**: Employees - **Required scope**: `employees:read` — Read employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.stats Aggregated KPIs for your workforce: total employee count, active and inactive counts, and a breakdown by working-hours arrangement (`full_time`/`part_time`). Deactivated employees count in `total`/`inactive` but not as active seats. Returned as `{ "data": EmployeeStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `total` (integer, required) - `active` (integer, required) - `inactive` (integer, required) - `full_time` (integer, required) - `part_time` (integer, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/employees/{employee} — Update an employee - **Operation ID**: `public-api.v1.employees.update` - **Tag**: Employees - **Required scope**: `employees:write` — Create and update employees. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/employees/public-api.v1.employees.update Update an employee. Partial update: only fields present in the payload are modified; omitted fields keep their value. `hire_date` is immutable. Returns the updated employee. ## Path parameters - `employee` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 10 properties; none of them required. - `first_name` (string, optional, maxLength 100) — Employee first name. - `last_name` (string, optional, maxLength 100) — Employee last name. - `email` (string, optional, format: email, maxLength 255) — Employee email (unique within the company). - `employment_type` (string, optional, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, optional) — Weekly contracted hours (greater than 0 and up to 168). - `ccaa` (string, optional, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community or city (ISO 3166-2:ES) used to localise public holidays. - `tax_id` (string | null, optional, maxLength 20) — Spanish fiscal identifier (NIF/NIE) of the employee (optional). - `job_title` (string | null, optional, maxLength 120) — Role or position of the employee (optional). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM), unique per company - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. ## Responses - **200** - Body (`application/json`): - `data` (object (Employee), required) — An employee of your company. Active employees count towards the seat billing of the workforce module; deactivated ones are preserved with their termination date. - `id` (string, required, format: uuid) — Opaque identifier of the employee, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `employee`) — Always `employee`. - `first_name` (string, required) — Given name of the employee. - `last_name` (string, required) — Family name of the employee. - `email` (string, required, format: email) — Contact email of the employee. - `tax_id` (string | null, required) — Spanish fiscal identifier (NIF/NIE) of the employee, or `null` when unknown. Not a foreign key. - `job_title` (string | null, required) — Role or position of the employee, or `null` when unset. - `employment_type` (string, required, enum: `full_time`, `part_time`) — Working-hours arrangement: `full_time` or `part_time`. - `contract_hours` (number, required, format: float) — Weekly contracted hours (e.g. `40` for a full-time schedule). - `hire_date` (string, required, format: date) — Calendar date the employee was hired, in `Y-m-d`. - `termination_date` (string | null, required, format: date) — Calendar date the employee was deactivated, in `Y-m-d`; `null` while active. - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Spanish autonomous community (comunidad autónoma) the employee is assigned to, as its two-letter code. - `status` (string, required, enum: `active`, `inactive`) — Lifecycle status: `active` (in the workforce) or `inactive` (deactivated). - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/HR system) mapping this employee to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `user_id` (string | null, required, format: uuid) — UUID v7 of the portal user linked to this employee, or `null` when no linkage exists yet. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/event-catalog — List event types - **Operation ID**: `public-api.v1.event_catalog.list` - **Tag**: Events - **Required scope**: `events:read` — Read events. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/events/public-api.v1.event_catalog.list List the closed catalog of event types Factuarea can emit to webhooks. Each entry exposes its `name`, `category`, a description and a `status`: `available` types are emitted today and subscribable via `enabled_events`; `coming_soon` types are reserved for a future release and not yet subscribable (passing one in `enabled_events` returns 422). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `name` (string, required) — Event identifier (e.g. `invoice.created`, `quote.approved`). - `category` (string, required) — High-level group derived from the prefix (`invoice`, `quote`, `client`, …). - `description` (string, required) — Human-readable description in Spanish. - `status` (string, required, enum: `available`, `coming_soon`) — `available` if the event is emitted today; `coming_soon` if reserved for a future release. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/events — List all events - **Operation ID**: `public-api.v1.events.list` - **Tag**: Events - **Required scope**: `events:read` — Read events. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/events/public-api.v1.events.list List events in your event log with cursor-based pagination. Each event records something that happened in your account (an invoice was paid, a quote accepted, …) and is the same object delivered to your webhook endpoints. Supports filtering by `type[in]` and `created[gte|lte]`. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `type` (string, optional) — Event type (e.g. invoice.created). - `type[in]` (string, optional) — Event type (e.g. invoice.created). - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the event, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). Consistent with the `id` field of every other v1 resource. - `object` (string, required, enum: `event`) — Always `event`. - `type` (string, required) — Event type in `.` form (e.g. `invoice.paid`, `quote.approved`). See `GET /event-catalog` for the full list. - `aggregate_id` (string | null, required, format: uuid) — UUID v7 of the resource that produced this event (e.g. the invoice for `invoice.paid`). `null` for events without backfill. Distinct from `id`, which identifies the event itself. - `correlation_id` (string | null, required, format: uuid) — UUID v7 correlating this event end-to-end with the operation that produced it. `null` for events without a correlation context (e.g. born from scheduler sweeps). The key is always present. - `api_version` (string | null, required) — API version (date-based) the payload was serialized under, sealed at emission (e.g. `2026-05-22`). `null` only for legacy events emitted before versions were sealed. - `livemode` (boolean, required) — `true` for events generated in production (live keys, `fact_live_`); `false` for events generated in test mode (sandbox company, test keys `fact_test_`). - `test` (boolean, required) — `true` when the event is a test delivery triggered from the dashboard; `false` for real events. Orthogonal to `livemode`, which reflects the key environment (live vs sandbox): a test delivery may be issued in either, so e.g. `livemode: true, test: true` is valid. - `data` (object (EventDataInvoiceCreated) | object (EventDataInvoiceAutoCreated) | object (EventDataInvoiceCorrectiveAutoCreated) | object (EventDataInvoiceSubscriptionAutoCreated) | object (EventDataInvoiceUpdated) | object (EventDataInvoiceSent) | object (EventDataInvoicePaid) | object (EventDataInvoiceCancelled) | object (EventDataInvoiceAnnulled) | object (EventDataInvoiceOverdue) | object (EventDataInvoiceDeleted) | object (EventDataInvoiceNumberAssigned) | object (EventDataInvoiceRectified) | object (EventDataInvoiceEmailSent) | object (EventDataInvoiceEmailFailed) | object (EventDataInvoicePaymentReminderSent) | object (EventDataInvoiceSimplifiedCreated) | object (EventDataInvoiceSimplifiedSubstituted) | object (EventDataInvoiceSubstitutedByComplete) | object (EventDataInvoiceVerifactuSubmitted) | object (EventDataInvoiceVerifactuFailed) | object (EventDataInvoiceMetadataChanged) | object (EventDataQuoteCreated) | object (EventDataQuoteUpdated) | object (EventDataQuoteDeleted) | object (EventDataQuoteApproved) | object (EventDataQuoteRejected) | object (EventDataQuoteConverted) | object (EventDataQuoteExpired) | object (EventDataQuoteMarkedAsPending) | object (EventDataQuoteCancelled) | object (EventDataQuoteNumberAssigned) | object (EventDataQuoteMetadataChanged) | object (EventDataQuoteEmailSent) | object (EventDataQuoteEmailFailed) | object (EventDataProformaCreated) | object (EventDataProformaUpdated) | object (EventDataProformaDeleted) | object (EventDataProformaAccepted) | object (EventDataProformaRejected) | object (EventDataProformaCancelled) | object (EventDataProformaExpired) | object (EventDataProformaConvertedToInvoice) | object (EventDataProformaNumberAssigned) | object (EventDataProformaMetadataChanged) | object (EventDataProformaEmailSent) | object (EventDataProformaEmailFailed) | object (EventDataDeliveryNoteCreated) | object (EventDataDeliveryNoteUpdated) | object (EventDataDeliveryNoteStatusChanged) | object (EventDataDeliveryNoteSigned) | object (EventDataDeliveryNoteConverted) | object (EventDataDeliveryNoteEmailSent) | object (EventDataDeliveryNoteEmailFailed) | object (EventDataPurchaseInvoiceCreated) | object (EventDataPurchaseInvoiceUpdated) | object (EventDataPurchaseInvoicePaid) | object (EventDataPurchaseInvoiceCancelled) | object (EventDataPurchaseInvoiceMetadataChanged) | object (EventDataPurchaseInvoicePaymentRegistered) | object (EventDataRecurringInvoiceCreated) | object (EventDataRecurringInvoiceActivated) | object (EventDataRecurringInvoicePaused) | object (EventDataRecurringInvoiceUpdated) | object (EventDataRecurringInvoiceDeleted) | object (EventDataRecurringInvoiceCompleted) | object (EventDataRecurringInvoiceExecuted) | object (EventDataRecurringInvoiceFailed) | object (EventDataRecurringInvoiceMetadataChanged) | object (EventDataRecurringInvoiceCancelled) | object (EventDataClientCreated) | object (EventDataClientUpdated) | object (EventDataClientDeleted) | object (EventDataClientMetadataChanged) | object (EventDataProductCreated) | object (EventDataProductUpdated) | object (EventDataPaymentReceived) | object (EventDataTaxMetadataChanged) | object (EventDataTaxValidityChanged) | object (EventDataTaxExternalReferenceChanged) | object (EventDataSeriesCreated) | object (EventDataSeriesUpdated) | object (EventDataSeriesDeleted) | object (EventDataSeriesArchived) | object (EventDataSeriesUnarchived) | object (EventDataSeriesMarkedAsDefault) | object (EventDataSeriesDemotedFromDefault) | object (EventDataSeriesYearReset) | object (EventDataSeriesMonthReset) | object (EventDataSeriesNumberConsumed) | object (EventDataFacturaeFaceSubmitted) | object (EventDataFacturaeFaceStatusChanged) | object (EventDataFacturaeFaceCancellationRequested) | object (EventDataPayoutReconciled) | object (EventDataEmployeeCreated) | object (EventDataEmployeeUpdated) | object (EventDataEmployeeDeactivated) | object (EventDataEmployeeInvited) | object (EventDataTimeEntryRecorded) | object (EventDataTimeEntryCorrected) | object (EventDataAbsenceRequested) | object (EventDataAbsenceApproved) | object (EventDataAbsenceRejected) | object (EventDataMonthlyRegisterClosed), required) — The payload embedded when the event was emitted, discriminated by `type`. Carries the full snapshot of the affected resource under `object` — identical to `GET /v1//{id}` at emission time — plus event-specific keys, e.g. `{ "type": "invoice.paid", "object": { ...invoice... }, "amount": 1210.0 }`. The snapshot is frozen: later changes to the resource do not rewrite past events. - `created` (integer, required) — Unix timestamp (seconds) of when the event was created. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/events/{event} — Retrieve an event - **Operation ID**: `public-api.v1.events.show` - **Tag**: Events - **Required scope**: `events:read` — Read events. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/events/public-api.v1.events.show Retrieve a single event by its `id` (format `evt_`, an opaque identifier). Useful for auditing and replaying webhook payloads. Returns `404 not_found` if the event does not exist or belongs to another company. ## Path parameters - `event` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Event), required) — A read-only record of something that happened in your account. The same object is delivered to your webhook endpoints. - `id` (string, required, format: uuid) — Opaque identifier of the event, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). Consistent with the `id` field of every other v1 resource. - `object` (string, required, enum: `event`) — Always `event`. - `type` (string, required) — Event type in `.` form (e.g. `invoice.paid`, `quote.approved`). See `GET /event-catalog` for the full list. - `aggregate_id` (string | null, required, format: uuid) — UUID v7 of the resource that produced this event (e.g. the invoice for `invoice.paid`). `null` for events without backfill. Distinct from `id`, which identifies the event itself. - `correlation_id` (string | null, required, format: uuid) — UUID v7 correlating this event end-to-end with the operation that produced it. `null` for events without a correlation context (e.g. born from scheduler sweeps). The key is always present. - `api_version` (string | null, required) — API version (date-based) the payload was serialized under, sealed at emission (e.g. `2026-05-22`). `null` only for legacy events emitted before versions were sealed. - `livemode` (boolean, required) — `true` for events generated in production (live keys, `fact_live_`); `false` for events generated in test mode (sandbox company, test keys `fact_test_`). - `test` (boolean, required) — `true` when the event is a test delivery triggered from the dashboard; `false` for real events. Orthogonal to `livemode`, which reflects the key environment (live vs sandbox): a test delivery may be issued in either, so e.g. `livemode: true, test: true` is valid. - `data` (object (EventDataInvoiceCreated) | object (EventDataInvoiceAutoCreated) | object (EventDataInvoiceCorrectiveAutoCreated) | object (EventDataInvoiceSubscriptionAutoCreated) | object (EventDataInvoiceUpdated) | object (EventDataInvoiceSent) | object (EventDataInvoicePaid) | object (EventDataInvoiceCancelled) | object (EventDataInvoiceAnnulled) | object (EventDataInvoiceOverdue) | object (EventDataInvoiceDeleted) | object (EventDataInvoiceNumberAssigned) | object (EventDataInvoiceRectified) | object (EventDataInvoiceEmailSent) | object (EventDataInvoiceEmailFailed) | object (EventDataInvoicePaymentReminderSent) | object (EventDataInvoiceSimplifiedCreated) | object (EventDataInvoiceSimplifiedSubstituted) | object (EventDataInvoiceSubstitutedByComplete) | object (EventDataInvoiceVerifactuSubmitted) | object (EventDataInvoiceVerifactuFailed) | object (EventDataInvoiceMetadataChanged) | object (EventDataQuoteCreated) | object (EventDataQuoteUpdated) | object (EventDataQuoteDeleted) | object (EventDataQuoteApproved) | object (EventDataQuoteRejected) | object (EventDataQuoteConverted) | object (EventDataQuoteExpired) | object (EventDataQuoteMarkedAsPending) | object (EventDataQuoteCancelled) | object (EventDataQuoteNumberAssigned) | object (EventDataQuoteMetadataChanged) | object (EventDataQuoteEmailSent) | object (EventDataQuoteEmailFailed) | object (EventDataProformaCreated) | object (EventDataProformaUpdated) | object (EventDataProformaDeleted) | object (EventDataProformaAccepted) | object (EventDataProformaRejected) | object (EventDataProformaCancelled) | object (EventDataProformaExpired) | object (EventDataProformaConvertedToInvoice) | object (EventDataProformaNumberAssigned) | object (EventDataProformaMetadataChanged) | object (EventDataProformaEmailSent) | object (EventDataProformaEmailFailed) | object (EventDataDeliveryNoteCreated) | object (EventDataDeliveryNoteUpdated) | object (EventDataDeliveryNoteStatusChanged) | object (EventDataDeliveryNoteSigned) | object (EventDataDeliveryNoteConverted) | object (EventDataDeliveryNoteEmailSent) | object (EventDataDeliveryNoteEmailFailed) | object (EventDataPurchaseInvoiceCreated) | object (EventDataPurchaseInvoiceUpdated) | object (EventDataPurchaseInvoicePaid) | object (EventDataPurchaseInvoiceCancelled) | object (EventDataPurchaseInvoiceMetadataChanged) | object (EventDataPurchaseInvoicePaymentRegistered) | object (EventDataRecurringInvoiceCreated) | object (EventDataRecurringInvoiceActivated) | object (EventDataRecurringInvoicePaused) | object (EventDataRecurringInvoiceUpdated) | object (EventDataRecurringInvoiceDeleted) | object (EventDataRecurringInvoiceCompleted) | object (EventDataRecurringInvoiceExecuted) | object (EventDataRecurringInvoiceFailed) | object (EventDataRecurringInvoiceMetadataChanged) | object (EventDataRecurringInvoiceCancelled) | object (EventDataClientCreated) | object (EventDataClientUpdated) | object (EventDataClientDeleted) | object (EventDataClientMetadataChanged) | object (EventDataProductCreated) | object (EventDataProductUpdated) | object (EventDataPaymentReceived) | object (EventDataTaxMetadataChanged) | object (EventDataTaxValidityChanged) | object (EventDataTaxExternalReferenceChanged) | object (EventDataSeriesCreated) | object (EventDataSeriesUpdated) | object (EventDataSeriesDeleted) | object (EventDataSeriesArchived) | object (EventDataSeriesUnarchived) | object (EventDataSeriesMarkedAsDefault) | object (EventDataSeriesDemotedFromDefault) | object (EventDataSeriesYearReset) | object (EventDataSeriesMonthReset) | object (EventDataSeriesNumberConsumed) | object (EventDataFacturaeFaceSubmitted) | object (EventDataFacturaeFaceStatusChanged) | object (EventDataFacturaeFaceCancellationRequested) | object (EventDataPayoutReconciled) | object (EventDataEmployeeCreated) | object (EventDataEmployeeUpdated) | object (EventDataEmployeeDeactivated) | object (EventDataEmployeeInvited) | object (EventDataTimeEntryRecorded) | object (EventDataTimeEntryCorrected) | object (EventDataAbsenceRequested) | object (EventDataAbsenceApproved) | object (EventDataAbsenceRejected) | object (EventDataMonthlyRegisterClosed), required) — The payload embedded when the event was emitted, discriminated by `type`. Carries the full snapshot of the affected resource under `object` — identical to `GET /v1//{id}` at emission time — plus event-specific keys, e.g. `{ "type": "invoice.paid", "object": { ...invoice... }, "amount": 1210.0 }`. The snapshot is frozen: later changes to the resource do not rewrite past events. - `created` (integer, required) — Unix timestamp (seconds) of when the event was created. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/face-submissions/{faceSubmission}/cancel — Request FACe submission cancellation - **Operation ID**: `public-api.v1.face_submissions.cancel` - **Tag**: FacturaE - **Required scope**: `facturae:write` — Create and update facturae. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/facturae/public-api.v1.face_submissions.cancel Requests the cancellation (anulación 4200) of a FACe submission with a mandatory `reason`. Only allowed while the submission is in a cancellable state (`submitted`, `registered_rcf`, `accounted`); otherwise returns 422 `face_submission_not_cancellable`. The submission transitions to `cancellation_requested` until FACe confirms. ## Path parameters - `faceSubmission` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `reason`. Public REST API v1 — POST /v1/face-submissions/{faceSubmission}/cancel. The cancellation reason (`reason`) is required: it travels to the FACe web service alongside the cancellation request (code 4200). Same contract as the SPA surface (`CancelFaceSubmissionRequest`). - `reason` (string, required, maxLength 255) ## Responses - **200** - Body (`application/json`): - `data` (object (FaceSubmission), required) — A submission of an invoice to FACe (the Spanish B2G general entry point). Tracks the FACe registry number and the processing status reported by the FACe web service. - `id` (string, required) — UUID (v7) of the FACe submission. - `object` (string, required, enum: `face_submission`) - `invoice_id` (string, required) — UUID (v7) of the submitted invoice. - `status` (string, required) — FACe processing status (`submitted`, `registered_rcf`, `accounted`, `paid`, `rejected`, `cancellation_requested`, `cancelled`, `error`). Kept up to date by the system polling — there is no refresh endpoint in v1. - `registry_number` (string | null, required) — Registry number assigned by FACe on presentation. Synthetic `FACE-SANDBOX-*` for sandbox (test key) submissions. - `dir3_accounting_office` (string, required) — DIR3 code of the accounting office (oficina contable, role 01) snapshotted at submission time. - `dir3_managing_body` (string, required) — DIR3 code of the managing body (órgano gestor, role 02) snapshotted at submission time. - `dir3_processing_unit` (string, required) — DIR3 code of the processing unit (unidad tramitadora, role 03) snapshotted at submission time. - `error_code` (string | null, required) — Local transmission error code, `null` unless the submission is in `error` state. - `error_message` (string | null, required) — Human-readable transmission error message (in Spanish), `null` unless the submission is in `error` state. - `status_updated_at` (string, required, format: date-time) — When the processing status last changed (ISO 8601). - `last_polled_at` (string | null, required, format: date-time) — When the system last polled FACe for this submission, or `null` if never polled yet. - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/face-submissions/{faceSubmission} — Retrieve a FACe submission - **Operation ID**: `public-api.v1.face_submissions.show` - **Tag**: FacturaE - **Required scope**: `facturae:read` — Read facturae. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/facturae/public-api.v1.face_submissions.show Retrieves a FACe submission by its `id` (UUID). The `status` field reflects the latest known FACe processing state (`submitted`, `registered_rcf`, `accounted`, `paid`, `rejected`, `cancellation_requested`, `cancelled`, `error`) — the system polls FACe periodically, so a plain GET is the way to track progress (there is no refresh endpoint in v1). ## Path parameters - `faceSubmission` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (FaceSubmission), required) — A submission of an invoice to FACe (the Spanish B2G general entry point). Tracks the FACe registry number and the processing status reported by the FACe web service. - `id` (string, required) — UUID (v7) of the FACe submission. - `object` (string, required, enum: `face_submission`) - `invoice_id` (string, required) — UUID (v7) of the submitted invoice. - `status` (string, required) — FACe processing status (`submitted`, `registered_rcf`, `accounted`, `paid`, `rejected`, `cancellation_requested`, `cancelled`, `error`). Kept up to date by the system polling — there is no refresh endpoint in v1. - `registry_number` (string | null, required) — Registry number assigned by FACe on presentation. Synthetic `FACE-SANDBOX-*` for sandbox (test key) submissions. - `dir3_accounting_office` (string, required) — DIR3 code of the accounting office (oficina contable, role 01) snapshotted at submission time. - `dir3_managing_body` (string, required) — DIR3 code of the managing body (órgano gestor, role 02) snapshotted at submission time. - `dir3_processing_unit` (string, required) — DIR3 code of the processing unit (unidad tramitadora, role 03) snapshotted at submission time. - `error_code` (string | null, required) — Local transmission error code, `null` unless the submission is in `error` state. - `error_message` (string | null, required) — Human-readable transmission error message (in Spanish), `null` unless the submission is in `error` state. - `status_updated_at` (string, required, format: date-time) — When the processing status last changed (ISO 8601). - `last_polled_at` (string | null, required, format: date-time) — When the system last polled FACe for this submission, or `null` if never polled yet. - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/face-submissions — List invoice FACe submissions - **Operation ID**: `public-api.v1.invoices.face_submissions.list` - **Tag**: FacturaE - **Required scope**: `facturae:read` — Read facturae. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/facturae/public-api.v1.invoices.face_submissions.list Lists the FACe submission history of an invoice (flat array, newest included). Returns `data: []` when the invoice has never been submitted. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) — UUID (v7) of the FACe submission. - `object` (string, required, enum: `face_submission`) - `invoice_id` (string, required) — UUID (v7) of the submitted invoice. - `status` (string, required) — FACe processing status (`submitted`, `registered_rcf`, `accounted`, `paid`, `rejected`, `cancellation_requested`, `cancelled`, `error`). Kept up to date by the system polling — there is no refresh endpoint in v1. - `registry_number` (string | null, required) — Registry number assigned by FACe on presentation. Synthetic `FACE-SANDBOX-*` for sandbox (test key) submissions. - `dir3_accounting_office` (string, required) — DIR3 code of the accounting office (oficina contable, role 01) snapshotted at submission time. - `dir3_managing_body` (string, required) — DIR3 code of the managing body (órgano gestor, role 02) snapshotted at submission time. - `dir3_processing_unit` (string, required) — DIR3 code of the processing unit (unidad tramitadora, role 03) snapshotted at submission time. - `error_code` (string | null, required) — Local transmission error code, `null` unless the submission is in `error` state. - `error_message` (string | null, required) — Human-readable transmission error message (in Spanish), `null` unless the submission is in `error` state. - `status_updated_at` (string, required, format: date-time) — When the processing status last changed (ISO 8601). - `last_polled_at` (string | null, required, format: date-time) — When the system last polled FACe for this submission, or `null` if never polled yet. - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/face-submissions — Submit invoice to FACe - **Operation ID**: `public-api.v1.invoices.face_submissions.submit` - **Tag**: FacturaE - **Required scope**: `facturae:write` — Create and update facturae. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/facturae/public-api.v1.invoices.face_submissions.submit Submit an issued invoice to FACe (the Spanish B2G entry point). Requires the client's three DIR3 codes and an active signing certificate; the FacturaE 3.2.2 XML is signed XAdES-EPES and presented to FACe, returning the registry number. No request body — the DIR3 codes are read from the client. Test keys simulate the submission without contacting FACe. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** - Body (`application/json`): - `data` (object (FaceSubmission), required) — A submission of an invoice to FACe (the Spanish B2G general entry point). Tracks the FACe registry number and the processing status reported by the FACe web service. - `id` (string, required) — UUID (v7) of the FACe submission. - `object` (string, required, enum: `face_submission`) - `invoice_id` (string, required) — UUID (v7) of the submitted invoice. - `status` (string, required) — FACe processing status (`submitted`, `registered_rcf`, `accounted`, `paid`, `rejected`, `cancellation_requested`, `cancelled`, `error`). Kept up to date by the system polling — there is no refresh endpoint in v1. - `registry_number` (string | null, required) — Registry number assigned by FACe on presentation. Synthetic `FACE-SANDBOX-*` for sandbox (test key) submissions. - `dir3_accounting_office` (string, required) — DIR3 code of the accounting office (oficina contable, role 01) snapshotted at submission time. - `dir3_managing_body` (string, required) — DIR3 code of the managing body (órgano gestor, role 02) snapshotted at submission time. - `dir3_processing_unit` (string, required) — DIR3 code of the processing unit (unidad tramitadora, role 03) snapshotted at submission time. - `error_code` (string | null, required) — Local transmission error code, `null` unless the submission is in `error` state. - `error_message` (string | null, required) — Human-readable transmission error message (in Spanish), `null` unless the submission is in `error` state. - `status_updated_at` (string, required, format: date-time) — When the processing status last changed (ISO 8601). - `last_polled_at` (string | null, required, format: date-time) — When the system last polled FACe for this submission, or `null` if never polled yet. - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/facturae — Download FacturaE XML - **Operation ID**: `public-api.v1.invoices.facturae` - **Tag**: FacturaE - **Required scope**: `facturae:read` — Read facturae. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/facturae/public-api.v1.invoices.facturae Stream the FacturaE 3.2.2 XML for the invoice (B2G compliance), XSD-conformant with full tax breakdown. With an active signing certificate the body is signed XAdES-EPES and served as `.xsig`; without one it is returned unsigned as `.xml`. The `X-Facturae-Signed` header distinguishes the two. Draft invoices return 422. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/xml`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/holidays — List all holidays - **Operation ID**: `public-api.v1.holidays.list` - **Tag**: Holidays - **Required scope**: `holidays:read` — Read holidays. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/holidays/public-api.v1.holidays.list List the public holidays visible to your company with cursor-based pagination: global reference holidays (national and per autonomous community, seeded and read-only) plus your custom local holidays. Supports filtering by `year`, `ccaa` (ISO 3166-2:ES autonomous community), `scope` (`national`/`autonomic`/`local`) and `source` (`reference` for seeded rows, `custom` for your own). ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `year` (integer, optional) — Calendar year of the holiday. - `ccaa` (string, optional) — Spanish autonomous community code (ISO 3166-2:ES) — matches autonomic holidays of that community. - `ccaa[in]` (string, optional) — Spanish autonomous community code (ISO 3166-2:ES) — matches autonomic holidays of that community. - `scope` (string, optional, enum: `national`, `autonomic`, `local`) — Territorial scope of the holiday. - `scope[in]` (string, optional) — Territorial scope of the holiday. - `source` (string, optional, enum: `reference`, `custom`) — Origin of the holiday: `reference` (seeded national/autonomic) or `custom` (company-defined local). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the holiday, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `holiday`) — Always `holiday`. - `scope` (string, required, enum: `national`, `autonomic`, `local`) — Territorial scope: `national` (all of Spain), `autonomic` (a single autonomous community) or `local` (a custom holiday of your company). - `community_code` (string | null, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`, `null`) — Autonomous community (comunidad autónoma) two-letter code (ISO 3166-2:ES) when `scope` is `autonomic`; `null` for `national` and `local` holidays. - `date` (string, required, format: date) — Calendar date of the holiday, in `Y-m-d`. - `year` (integer, required) — Year the holiday falls on (derived from `date`). - `name` (string, required) — Human-readable name of the holiday (e.g. `Año Nuevo`). - `is_system` (boolean, required) — Whether this is a seeded reference holiday (`true`, read-only for your company) or a custom local holiday you created (`false`). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/holidays/resolve — Resolve applicable holidays - **Operation ID**: `public-api.v1.holidays.resolve` - **Tag**: Holidays - **Required scope**: `holidays:read` — Read holidays. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/holidays/public-api.v1.holidays.resolve Resolve the holidays that apply to a given autonomous community in a given year: national holidays, the autonomic holidays of that `ccaa`, and your custom local holidays, merged into a single flat list under `{ "data": [Holiday, …] }`. Both `ccaa` (ISO 3166-2:ES) and `year` are required; an invalid community code or an out-of-range year returns 422. ## Query parameters - `ccaa` (string, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`) — Autonomous community or city (ISO 3166-2:ES). - `year` (integer, required, min 2000, max 2100) — Year of the holidays to resolve. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the holiday, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `holiday`) — Always `holiday`. - `scope` (string, required, enum: `national`, `autonomic`, `local`) — Territorial scope: `national` (all of Spain), `autonomic` (a single autonomous community) or `local` (a custom holiday of your company). - `community_code` (string | null, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`, `null`) — Autonomous community (comunidad autónoma) two-letter code (ISO 3166-2:ES) when `scope` is `autonomic`; `null` for `national` and `local` holidays. - `date` (string, required, format: date) — Calendar date of the holiday, in `Y-m-d`. - `year` (integer, required) — Year the holiday falls on (derived from `date`). - `name` (string, required) — Human-readable name of the holiday (e.g. `Año Nuevo`). - `is_system` (boolean, required) — Whether this is a seeded reference holiday (`true`, read-only for your company) or a custom local holiday you created (`false`). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/holidays/{holiday} — Retrieve a holiday - **Operation ID**: `public-api.v1.holidays.show` - **Tag**: Holidays - **Required scope**: `holidays:read` — Read holidays. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/holidays/public-api.v1.holidays.show Retrieve a single holiday by its `id` (UUID v7). A custom holiday belonging to another company returns 404 `holiday_not_found` (anti-enumeration). ## Path parameters - `holiday` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Holiday), required) — A public holiday visible to your company: a global reference holiday (national or per autonomous community, seeded and read-only) or a custom local holiday you created. Used by the Control Horario (time tracking) module to exclude non-working days. - `id` (string, required, format: uuid) — Opaque identifier of the holiday, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `holiday`) — Always `holiday`. - `scope` (string, required, enum: `national`, `autonomic`, `local`) — Territorial scope: `national` (all of Spain), `autonomic` (a single autonomous community) or `local` (a custom holiday of your company). - `community_code` (string | null, required, enum: `AN`, `AR`, `AS`, `IB`, `CN`, `CB`, `CL`, `CM`, `CT`, `VC`, `EX`, `GA`, `MD`, `MC`, `NC`, `PV`, `RI`, `CE`, `ML`, `null`) — Autonomous community (comunidad autónoma) two-letter code (ISO 3166-2:ES) when `scope` is `autonomic`; `null` for `national` and `local` holidays. - `date` (string, required, format: date) — Calendar date of the holiday, in `Y-m-d`. - `year` (integer, required) — Year the holiday falls on (derived from `date`). - `name` (string, required) — Human-readable name of the holiday (e.g. `Año Nuevo`). - `is_system` (boolean, required) — Whether this is a seeded reference holiday (`true`, read-only for your company) or a custom local holiday you created (`false`). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/integrations/events — List integration events - **Operation ID**: `public-api.v1.integrations.events.list` - **Tag**: Integration Events - **Required scope**: `integration_events:read` — Read integration events. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/integration-events/public-api.v1.integrations.events.list Browse everything the payment gateways have sent to Factuarea. This is the inbox to open when a charge did not generate its invoice: every discarded event carries a typed `discard_reason` and whether it can be reprocessed. Newest first, and scoped to the authenticated company. The raw content of the event is never returned. ## Query parameters - `provider` (string, optional, enum: `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl`) — Payment gateway or integration that produced the event. - `status` (string, optional, enum: `success`, `skipped`, `failure`) — Outcome of the event (success, skipped, failure). - `event_type` (string, optional, maxLength 100) — Provider event type, matched exactly. - `discard_reason` (string, optional, enum: `event_not_normalizable`, `duplicate_redelivery`, `connected_account_missing`, `connected_account_unknown`, `spontaneous_payment_missing_id`, `autoinvoicing_disabled`, `unsupported_currency`, `refund_without_items`, `refund_autoinvoicing_disabled`, `subscription_missing_invoice_id`, `subscription_proration_review`, `subscription_not_a_cycle`, `subscription_trial_skipped`, `subscription_autoinvoicing_disabled`, `subscription_already_invoiced`, `payout_missing_id`, `payout_connected_account_missing`, `payment_failed`, `event_type_not_covered`, `checkout_lines_retrieve_failed`) — Typed reason why the event was discarded, from the closed catalogue. - `is_parked` (boolean, optional) — Whether the event was parked with its content so it can be replayed. - `created_at[gte]` (string, optional, format: date-time) — Inclusive lower bound of created_at (ISO 8601). - `created_at[lte]` (string, optional, format: date-time) — Inclusive upper bound of created_at (ISO 8601). - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of events to return. - `starting_after` (string, optional, pattern: `^[0-9]+$`) — Cursor for forward pagination: pass back the `next_cursor` of the previous page. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `IntegrationEventListV1Resource` - Body (`application/json`): - `data` (array, required) — Page of integration events, newest first. The elements do NOT carry `recommended_action`: fetch the detail of the one you care about. - `id` (string, required, format: uuid) — UUID v7 of the event. Use it in `GET /v1/integrations/events/{event}` and in the replay operation. - `object` (string, required, enum: `integration_event`) — Always `integration_event`. - `provider` (string, required, enum: `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl`) — Gateway or integration the event came from. Closed set, so it is safe to switch on it. - `event_type` (string, required) — Event type as reported by the provider, matched EXACTLY when you filter by it. Free-form text, NOT a closed enum: the field deliberately mixes the raw provider type (`charge.refunded`, `invoice.paid`) with the internal semantic value some older branches recorded (`autoinvoice.*`). Do not model it as a fixed catalogue — for a filterable, closed axis use `discard_reason`. - `direction` (string, required) — Direction of the exchange: `inbound` for something the provider sent us (all gateway webhooks), `outbound` for a call the platform made to the provider, `internal` for a state change of the integration itself. Free-form string rather than an enum: it is not backed by a closed catalogue in code. - `status` (string, required, enum: `success`, `skipped`, `failure`) — Outcome of the event: `success` (it produced its effect), `skipped` (it was discarded on purpose — see `discard_reason`) or `failure` (it broke while being processed — see `error_message`). - `discard_reason` (string | null, required, enum: `event_not_normalizable`, `duplicate_redelivery`, `connected_account_missing`, `connected_account_unknown`, `spontaneous_payment_missing_id`, `autoinvoicing_disabled`, `unsupported_currency`, `refund_without_items`, `refund_autoinvoicing_disabled`, `subscription_missing_invoice_id`, `subscription_proration_review`, `subscription_not_a_cycle`, `subscription_trial_skipped`, `subscription_autoinvoicing_disabled`, `subscription_already_invoiced`, `payout_missing_id`, `payout_connected_account_missing`, `payment_failed`, `event_type_not_covered`, `checkout_lines_retrieve_failed`, `null`) — Typed reason why the event was discarded, from a CLOSED catalogue, or `null` when it was not discarded. This is the axis to filter and group by. A value retired from the catalogue in a later version is still returned verbatim here, but its `discard_reason_label`, `is_actionable` and `is_replayable` degrade to neutral rather than breaking the page. - `discard_reason_label` (string | null, required) — Human-readable label of `discard_reason`, in Spanish (the platform's end-user language). `null` when there is no discard reason, or when the stored reason is no longer part of the current catalogue. - `is_actionable` (boolean, required) — `true` when the account owner can do something about this discard (turn a setting on, re-link an account). Only actionable discards notify the user; the informative ones are recorded but never interrupt anyone. - `is_replayable` (boolean, required) — CONTRACT of the replay operation: when it is `true`, `POST /v1/integrations/events/{event}/replay` does not answer 422. It is the conjunction of three conditions — the event is parked, it still holds its content, and its discard reason admits reprocessing — evaluated by the same handler that guards the replay. It turns to `false` on its own once the 30-day retention window purges the content, even though the event stays parked. - `error_message` (string | null, required) — Error raised while processing the event, or `null` when there was none. Relevant for `failure`; a `skipped` event carries its reason in `discard_reason`, not here. - `duration_ms` (integer | null, required) — Server-side processing time in milliseconds, or `null` when it was not measured. - `created_at` (string, required, format: date-time) — When the event was recorded (ISO 8601). - `recommended_action` (string | null, optional) — Next step for this specific discard reason, in Spanish and in the imperative. Present ONLY in the detail and in the 202 of the replay — the listing omits it, because repeating the same sentence across 100 elements is noise. `null` when the reason is merely informative: if there is nothing to do, the contract does not invent an instruction. The sentence tells reproducible reasons ("… y reprocesa el evento") apart from the ones that are not ("… emítela a mano"), so it never points you at a call that would answer 422. When a reason admits BOTH ways out, they are worded as mutually exclusive and the sentence spells out the consequence of doing both: replay idempotency keys on the charge and only recognises invoices issued through that same automatic path, so an invoice created by hand does not stop it — doing both leaves two invoices for one charge, each numbered in its series and filed with VeriFactu. - `has_more` (boolean, required) — `true` when more events exist beyond this page. - `next_cursor` (string | null, required) — Opaque cursor to pass as `starting_after` for the next page, or `null` when `has_more` is `false`. Unlike the rest of the v1 listings it is a numeric string, not a UUID v7 — treat it as opaque and reuse it verbatim. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/integrations/events/{event}/replay — Replay a parked integration event - **Operation ID**: `public-api.v1.integrations.events.replay` - **Tag**: Integration Events - **Required scope**: `integration_events:write` — Create and update integration events. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/integration-events/public-api.v1.integrations.events.replay Reprocess a gateway event that was parked, once the cause that prevented it from producing its effect is gone. **Before you call it** - The event must be published with `is_replayable` set to `true` — anything else returns 422. - Fix the cause first: turn automatic invoicing back on, re-link the connected account, wait for the exchange rate. - It needs the write scope `integration_events:write`, never the read scope of the inbox. **What it can do** > **This action can have real fiscal consequences.** If the cause is already resolved, the replay CAN ISSUE A REAL INVOICE, with its series number and its registration in VeriFactu. Confirm with the account owner before calling it. **What comes back** - `202` means accepted and queued, **not** completed. - The body returns the event as it stands now, not the outcome of the retry. - The outcome appears as a NEW event in the inbox — poll `GET /v1/integrations/events` to see how it ended. - It never duplicates invoices: the replay goes through the same idempotency check as the original attempt. ## Path parameters - `event` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **202** - Body (`application/json`): - `data` (object (IntegrationEvent), required) — One event a payment gateway sent to Factuarea, with the outcome it produced and, when it produced none, the typed reason why. Read-only and scoped to your own company. The raw content of the event is NEVER exposed: it carries personal data of your end customers and payment details, is stored encrypted only for parked events so they can be reprocessed, and is purged 30 days after being parked. - `id` (string, required, format: uuid) — UUID v7 of the event. Use it in `GET /v1/integrations/events/{event}` and in the replay operation. - `object` (string, required, enum: `integration_event`) — Always `integration_event`. - `provider` (string, required, enum: `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl`) — Gateway or integration the event came from. Closed set, so it is safe to switch on it. - `event_type` (string, required) — Event type as reported by the provider, matched EXACTLY when you filter by it. Free-form text, NOT a closed enum: the field deliberately mixes the raw provider type (`charge.refunded`, `invoice.paid`) with the internal semantic value some older branches recorded (`autoinvoice.*`). Do not model it as a fixed catalogue — for a filterable, closed axis use `discard_reason`. - `direction` (string, required) — Direction of the exchange: `inbound` for something the provider sent us (all gateway webhooks), `outbound` for a call the platform made to the provider, `internal` for a state change of the integration itself. Free-form string rather than an enum: it is not backed by a closed catalogue in code. - `status` (string, required, enum: `success`, `skipped`, `failure`) — Outcome of the event: `success` (it produced its effect), `skipped` (it was discarded on purpose — see `discard_reason`) or `failure` (it broke while being processed — see `error_message`). - `discard_reason` (string | null, required, enum: `event_not_normalizable`, `duplicate_redelivery`, `connected_account_missing`, `connected_account_unknown`, `spontaneous_payment_missing_id`, `autoinvoicing_disabled`, `unsupported_currency`, `refund_without_items`, `refund_autoinvoicing_disabled`, `subscription_missing_invoice_id`, `subscription_proration_review`, `subscription_not_a_cycle`, `subscription_trial_skipped`, `subscription_autoinvoicing_disabled`, `subscription_already_invoiced`, `payout_missing_id`, `payout_connected_account_missing`, `payment_failed`, `event_type_not_covered`, `checkout_lines_retrieve_failed`, `null`) — Typed reason why the event was discarded, from a CLOSED catalogue, or `null` when it was not discarded. This is the axis to filter and group by. A value retired from the catalogue in a later version is still returned verbatim here, but its `discard_reason_label`, `is_actionable` and `is_replayable` degrade to neutral rather than breaking the page. - `discard_reason_label` (string | null, required) — Human-readable label of `discard_reason`, in Spanish (the platform's end-user language). `null` when there is no discard reason, or when the stored reason is no longer part of the current catalogue. - `is_actionable` (boolean, required) — `true` when the account owner can do something about this discard (turn a setting on, re-link an account). Only actionable discards notify the user; the informative ones are recorded but never interrupt anyone. - `is_replayable` (boolean, required) — CONTRACT of the replay operation: when it is `true`, `POST /v1/integrations/events/{event}/replay` does not answer 422. It is the conjunction of three conditions — the event is parked, it still holds its content, and its discard reason admits reprocessing — evaluated by the same handler that guards the replay. It turns to `false` on its own once the 30-day retention window purges the content, even though the event stays parked. - `error_message` (string | null, required) — Error raised while processing the event, or `null` when there was none. Relevant for `failure`; a `skipped` event carries its reason in `discard_reason`, not here. - `duration_ms` (integer | null, required) — Server-side processing time in milliseconds, or `null` when it was not measured. - `created_at` (string, required, format: date-time) — When the event was recorded (ISO 8601). - `recommended_action` (string | null, optional) — Next step for this specific discard reason, in Spanish and in the imperative. Present ONLY in the detail and in the 202 of the replay — the listing omits it, because repeating the same sentence across 100 elements is noise. `null` when the reason is merely informative: if there is nothing to do, the contract does not invent an instruction. The sentence tells reproducible reasons ("… y reprocesa el evento") apart from the ones that are not ("… emítela a mano"), so it never points you at a call that would answer 422. When a reason admits BOTH ways out, they are worded as mutually exclusive and the sentence spells out the consequence of doing both: replay idempotency keys on the charge and only recognises invoices issued through that same automatic path, so an invoice created by hand does not stop it — doing both leaves two invoices for one charge, each numbered in its series and filed with VeriFactu. - **400** — The request is syntactically malformed — e.g. an unknown query parameter, an integer parameter with non-numeric value, or a value outside the documented range. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — The integration event exists but cannot be replayed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/integrations/events/{event} — Retrieve an integration event - **Operation ID**: `public-api.v1.integrations.events.show` - **Tag**: Integration Events - **Required scope**: `integration_events:read` — Read integration events. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/integration-events/public-api.v1.integrations.events.show Retrieve one integration event by its id, typically after finding it in the listing, to know exactly why a charge did not produce its invoice and what to do next. On top of the listing fields, the detail adds `recommended_action`, one imperative sentence with the next step, and `is_replayable`, which tells you whether the replay operation would accept the event. ## Path parameters - `event` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (IntegrationEvent), required) — One event a payment gateway sent to Factuarea, with the outcome it produced and, when it produced none, the typed reason why. Read-only and scoped to your own company. The raw content of the event is NEVER exposed: it carries personal data of your end customers and payment details, is stored encrypted only for parked events so they can be reprocessed, and is purged 30 days after being parked. - `id` (string, required, format: uuid) — UUID v7 of the event. Use it in `GET /v1/integrations/events/{event}` and in the replay operation. - `object` (string, required, enum: `integration_event`) — Always `integration_event`. - `provider` (string, required, enum: `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl`) — Gateway or integration the event came from. Closed set, so it is safe to switch on it. - `event_type` (string, required) — Event type as reported by the provider, matched EXACTLY when you filter by it. Free-form text, NOT a closed enum: the field deliberately mixes the raw provider type (`charge.refunded`, `invoice.paid`) with the internal semantic value some older branches recorded (`autoinvoice.*`). Do not model it as a fixed catalogue — for a filterable, closed axis use `discard_reason`. - `direction` (string, required) — Direction of the exchange: `inbound` for something the provider sent us (all gateway webhooks), `outbound` for a call the platform made to the provider, `internal` for a state change of the integration itself. Free-form string rather than an enum: it is not backed by a closed catalogue in code. - `status` (string, required, enum: `success`, `skipped`, `failure`) — Outcome of the event: `success` (it produced its effect), `skipped` (it was discarded on purpose — see `discard_reason`) or `failure` (it broke while being processed — see `error_message`). - `discard_reason` (string | null, required, enum: `event_not_normalizable`, `duplicate_redelivery`, `connected_account_missing`, `connected_account_unknown`, `spontaneous_payment_missing_id`, `autoinvoicing_disabled`, `unsupported_currency`, `refund_without_items`, `refund_autoinvoicing_disabled`, `subscription_missing_invoice_id`, `subscription_proration_review`, `subscription_not_a_cycle`, `subscription_trial_skipped`, `subscription_autoinvoicing_disabled`, `subscription_already_invoiced`, `payout_missing_id`, `payout_connected_account_missing`, `payment_failed`, `event_type_not_covered`, `checkout_lines_retrieve_failed`, `null`) — Typed reason why the event was discarded, from a CLOSED catalogue, or `null` when it was not discarded. This is the axis to filter and group by. A value retired from the catalogue in a later version is still returned verbatim here, but its `discard_reason_label`, `is_actionable` and `is_replayable` degrade to neutral rather than breaking the page. - `discard_reason_label` (string | null, required) — Human-readable label of `discard_reason`, in Spanish (the platform's end-user language). `null` when there is no discard reason, or when the stored reason is no longer part of the current catalogue. - `is_actionable` (boolean, required) — `true` when the account owner can do something about this discard (turn a setting on, re-link an account). Only actionable discards notify the user; the informative ones are recorded but never interrupt anyone. - `is_replayable` (boolean, required) — CONTRACT of the replay operation: when it is `true`, `POST /v1/integrations/events/{event}/replay` does not answer 422. It is the conjunction of three conditions — the event is parked, it still holds its content, and its discard reason admits reprocessing — evaluated by the same handler that guards the replay. It turns to `false` on its own once the 30-day retention window purges the content, even though the event stays parked. - `error_message` (string | null, required) — Error raised while processing the event, or `null` when there was none. Relevant for `failure`; a `skipped` event carries its reason in `discard_reason`, not here. - `duration_ms` (integer | null, required) — Server-side processing time in milliseconds, or `null` when it was not measured. - `created_at` (string, required, format: date-time) — When the event was recorded (ISO 8601). - `recommended_action` (string | null, optional) — Next step for this specific discard reason, in Spanish and in the imperative. Present ONLY in the detail and in the 202 of the replay — the listing omits it, because repeating the same sentence across 100 elements is noise. `null` when the reason is merely informative: if there is nothing to do, the contract does not invent an instruction. The sentence tells reproducible reasons ("… y reprocesa el evento") apart from the ones that are not ("… emítela a mano"), so it never points you at a call that would answer 422. When a reason admits BOTH ways out, they are worded as mutually exclusive and the sentence spells out the consequence of doing both: replay idempotency keys on the charge and only recognises invoices issued through that same automatic path, so an invoice created by hand does not stop it — doing both leaves two invoices for one charge, each numbered in its series and filed with VeriFactu. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/activities — List invoice activity - **Operation ID**: `public-api.v1.invoices.activities` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.activities Returns the cursor-paginated activity timeline (audit log) of a single invoice: status transitions, emails, reminders, and metadata changes. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `invoice.created`, `invoice.paid`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/annul — Annul an invoice - **Operation ID**: `public-api.v1.invoices.annul` - **Tag**: Invoices - **Required scope**: `invoices:void` — Void invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.annul Withdraw an issued invoice **with a documented reason**. `reason` is required here (3–500 characters); that is the only difference from `POST /v1/invoices/{id}/void`, which performs exactly the same operation and persists a placeholder when you omit it. Prefer this endpoint whenever the reason has to be traceable — the text you send is kept in the invoice audit trail and, when the company is enrolled in VeriFactu, becomes the `motivo` of the AEAT cancellation record. The invoice moves to `annulled` and `voided_at` starts reporting when that happened. The status is terminal and the operation is **irreversible**: there is no transition back to `sent` or `draft`, and the correlative number of the series is neither released nor reused. **Effect on AEAT.** With VeriFactu enabled, annulling queues an *anulación* record to AEAT **asynchronously**: a `200` means the invoice is annulled in Factuarea, not that AEAT has already processed it — poll the invoice for its VeriFactu status. The original *alta* record is not deleted or rewritten; AEAT keeps both entries, the issuance and its cancellation. With VeriFactu inactive the annulment is purely internal and nothing is transmitted. **Annul or correct?** Annulment withdraws the whole document and only works before payment; it produces no amending document, so it never restates an amount. A corrective (`POST /v1/invoices/{id}/corrective`) creates a **new** invoice that amends the original and is the only path for an invoice that is already `paid` or that is only partly wrong. Limits: only `sent` or `overdue` can be annulled. A `draft` is not annulled but deleted; `paid`, `cancelled` and `annulled` return 422. An invoice that **is** a corrective can never be annulled — issue a new corrective of the original instead. Use `GET /v1/invoices/{id}/can-annul` to check eligibility, and whether a VeriFactu cancellation record will be created, before posting here. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `reason`. Public REST API v1 — POST /v1/invoices/{uuid}/annul. Body: `reason` (string, required, max. 500). The reason is persisted on the Invoice aggregate and included in the VeriFactu cancellation record (AnulacionRecord) when applicable. - `reason` (string, required, maxLength 500, minLength 3) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/assign-real-number — Assign a real invoice number - **Operation ID**: `public-api.v1.invoices.assign_real_number` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.assign_real_number Promotes a draft to a definitive invoice by assigning its real series number. In VeriFactu-enabled companies the same happens automatically on send. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/bulk-create — Bulk create invoices - **Operation ID**: `public-api.v1.invoices.bulk_create` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.bulk_create Create up to 100 invoices in one call, each entry a full invoice payload. With `dry_run=true` it validates every row without persisting and returns a per-row classification (`results[]`, including duplicate `external_id` and a non-blocking AEAT census warning); with `dry_run=false` it creates only the valid rows and reports the rest in `failures[]`. Returns the `BulkCreateResult` shape. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `invoices`. Create invoices in bulk. `invoices[]` holds up to 100 invoice payloads and `dry_run` (default `false`) validates each row without persisting. Per-row rules (format, duplicate `external_id`, recipient AEAT census) are reported per row instead of failing the whole batch. - `dry_run` (boolean | null, optional) - `invoices` (array>, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkCreateResult), required) — Result of a bulk invoice creation (`POST /v1/invoices/bulk-create`). `dry_run` reports the mode. In validate-only mode (`dry_run=true`) `results[]` carries the per-row classification `{index, status, errors[], warnings[]}` and nothing is persisted; in create mode (`dry_run=false`) only valid rows are created and `failures[]` carries the rows that were not created (identified by `index`). `total = successful + failed`. - `dry_run` (boolean, required) — Whether the operation ran in validate-only mode (no invoice was created). - `total` (integer, required) — Number of rows processed (`successful + failed`). - `successful` (integer, required) — Number of rows that validated successfully (`dry_run=true`) or were created (`dry_run=false`). - `failed` (integer, required) — Number of rows that were invalid or could not be created. Equals `failures` length in create mode. - `results` (array, required) — Per-row classification of validate-only mode (`dry_run=true`). Empty in create mode. - `failures` (array, required) — Rows that could not be created (create mode, `dry_run=false`). Each item carries the 0-based `index`, an `error_code`, a Spanish `error_message` and the per-field `errors[]`. Empty in validate-only mode. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/bulk-delete — Bulk delete invoices - **Operation ID**: `public-api.v1.invoices.bulk_delete` - **Tag**: Invoices - **Required scope**: `invoices:delete` — Delete invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.bulk_delete Delete several invoices in one call with partial success — each id is evaluated independently and one failure never aborts the batch. Only draft invoices are deleted; an issued invoice comes back as a `resource_not_deletable` failure (use `void` instead). Returns a `BulkPartialSuccessResult` with `total`, `successful`, `failed` and a per-id `failures` list. ```json { "ids": ["0190a1b2-c3d4-7e5f-8a90-1b2c3d4e5f60", "0190a1b2-c3d4-7e5f-8a90-1b2c3d4e5f61"] } ``` Limits: `ids` accepts 1–100 UUID v7 entries per call. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Public REST API v1 — DELETE /v1/invoices/bulk. - `ids` (array, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/bulk-pdf — Bulk download invoice PDFs - **Operation ID**: `public-api.v1.invoices.bulk_pdf` - **Tag**: Invoices - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.bulk_pdf Packages the PDFs of up to 50 invoices (by id) into a single ZIP. Ids that are not found or have no generable PDF do not abort the request: the ZIP carries only the valid ones and the per-resource counts travel in the `X-Bulk-*` response headers. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Package the PDFs of several invoices into a single ZIP. `ids` is an array of invoice UUIDs, up to 50 per request. - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/zip`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/bulk-send — Bulk send invoices - **Operation ID**: `public-api.v1.invoices.bulk_send` - **Tag**: Invoices - **Required scope**: `invoices:send` — Send by email invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.bulk_send Sends up to 200 invoices by email (queued) in one call, reusing the single-send path per id. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each invoice that could not be sent (not found, terminal status or no resolvable recipient). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 1 required: `ids`. Email several invoices in one request (queued), up to 200 per batch. `ids` is an array of invoice UUIDs; the optional `to`/`cc` arrays and `subject`/`message`/`language` overrides apply to the whole batch (when `to` is omitted, each invoice uses its client email). - `subject` (string | null, optional, maxLength 200) - `message` (string | null, optional, maxLength 5000) - `language` (string | null, optional, maxLength 5) - `ids` (array, required, maxItems 200) - `to` (array | null, optional) - `cc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/bulk-status — Bulk change invoice status - **Operation ID**: `public-api.v1.invoices.bulk_status` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.bulk_status Transition several invoices to a new status in one call, each through the same Aggregate guard, with partial success (a rejected id never aborts the batch). `new_status` is `sent` or `paid`; when `paid`, `payment_date` is required and propagated as the real payment date of every invoice (never `now()`). Returns a `BulkPartialSuccessResult` with `total`, `successful`, `failed` and a per-id `failures` list (`resource_not_found` or `invalid_status_transition`). ```json { "ids": ["0190a1b2-c3d4-7e5f-8a90-1b2c3d4e5f60"], "new_status": "paid", "payment_date": "2026-06-30" } ``` Limits: `ids` accepts 1–50 entries; `payment_date` is required when `new_status` is `paid` and must not be in the future. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `new_status`, `ids`. Transition several invoices to `new_status` (`sent` or `paid`) in one request, up to 50 per batch. `ids` is an array of invoice UUIDs; `payment_date` is required and cannot be in the future when `new_status` is `paid`. Every transition passes the document state guard, and invoices that cannot transition are returned under `failures[]`. - `new_status` (string, required, enum: `sent`, `paid`) - `payment_date` (string | null, optional, format: date-time) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/can-annul — Check annulment eligibility - **Operation ID**: `public-api.v1.invoices.can_annul` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.can_annul Validates whether the invoice can be annulled and whether a VeriFactu anulacion record will be created. Call before posting to /annul. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (CanAnnulInvoice), required) — Result of the pre-cancellation validator of an invoice: whether it can be cancelled, the blocking reasons (when it cannot) and whether the cancellation will create an additional VeriFactu record. - `can_annul` (boolean, required) — Whether the invoice can be annulled in its current status. - `reasons` (array, required) — Blocking reasons when `can_annul` is `false`. Empty `[]` when the invoice can be cancelled. - `will_create_verifactu` (boolean, required) — Indicates whether the cancellation will generate a cancellation record in VeriFactu. - `info` (array, required) — Additional informational messages about the cancellation (non-blocking warnings). Empty `[]` when there are none. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/corrective — Generate corrective invoice - **Operation ID**: `public-api.v1.invoices.corrective` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.corrective Issue a corrective invoice (*rectificativa*, RD 1619/2012 art. 15) that amends a previously issued invoice. Returns `201` with the **new** invoice: `is_corrective: true`, `corrective` pointing at the original, and a number derived from the original's in the same series (`F-2026-0042-REC1`, `-REC2`… for successive correctives). Flow: the original must already be issued (`sent` or `paid`) → the corrective is born **already issued**, never as a draft → when VeriFactu is enabled its *alta* is transmitted to AEAT **asynchronously**, so a `201` does not mean AEAT has accepted it yet. The original is never modified: it keeps its number, its status and its own VeriFactu record. A corrective is an additional document, not an edit. **Full vs partial.** `correction_type: full` is a substitution (VeriFactu nature `S`): the `lines` you send are the *final correct amounts*, and omitting `lines` entirely turns it into a full cancellation, where every original line is copied back negated and prefixed `[ANULACION]`. `correction_type: partial` is a correction by differences (nature `I`): `lines` is required and each one is a delta — typically negative — prefixed `[AJUSTE]`. In a partial correction a line only moves stock if it declares its own `product_id`; in a substitution the product is inherited from the original line at the same index. **AEAT R code.** By default it is derived from `correction_reason`: `error_fundado` → R1, `concurso` → R2, `incobrable` → R3, everything else → R4; a corrective of a simplified (F2) invoice is always born R5 regardless of the reason. `correction_code` overrides that derivation, but is validated against the legal matrix — original F2 → only `R5`; original F1/F3 → only `R1`–`R4`. Any other combination returns 422 with the legal `allowed_values`. Limits: `draft`, `overdue`, `cancelled` and `annulled` originals return 422 (an `overdue` invoice must be paid or voided first); a corrective can never itself be corrected — issue a new corrective of the original instead. List every corrective of an invoice with `GET /v1/invoices/{id}/correctives`. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 8 properties; 2 required: `correction_reason`, `correction_type`. Generate a corrective (rectificativa) invoice for a previously issued invoice. `correction_reason` (required) maps to a VeriFactu R-code; `correction_type` is `full` or `partial`; the optional `correction_code` (`R1`..`R5`) forces the explicit R-code and is validated against the AEAT legal matrix for the original invoice type. Optional `justification`, `notes`, and `lines[]` (required when `correction_type` is `partial`). - `correction_reason` (string, required, enum: `error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`) - `correction_type` (string, required, enum: `full`, `partial`) - `correction_code` (string | null, optional, enum: `R1`, `R2`, `R3`, `R4`, `R5`) - `justification` (string | null, optional, maxLength 1000, minLength 10) - `notes` (string | null, optional, maxLength 1000) - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional) - `unit_price` (number, optional, min 0) - `tax_rate` (number | null, optional, min 0, max 100) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) - `product_id` (string | null, optional, format: uuid) - `line_type` (string | null, optional, enum: `NORMAL`, `SUPLIDO`) — Kind of line: `NORMAL` (default) for an ordinary line, or `SUPLIDO` for a DISBURSEMENT — an amount paid in the name and on behalf of the client (an official fee, duty or registry charge) re-invoiced at cost, which stays out of the taxable base (art. 78.Tres.3 LIVA) and is aggregated into `total_disbursements`. A `SUPLIDO` line must carry no VAT, withholding, surcharge, discount or product, and requires `source_invoice_reference`. Only meaningful when `correction_type` is `partial`, which is when `lines[]` is sent; a value outside the catalog is rejected with 422. - `source_invoice_reference` (string | null, optional, maxLength 100) ## Responses - **201** — Corrective invoice created successfully. The `Location` header contains the canonical URL of the NEW corrective invoice. - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/correctives — List corrective invoices - **Operation ID**: `public-api.v1.invoices.correctives` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.correctives Returns all corrective invoices associated with the original invoice. Used to reconstruct the original → rectificativa tree. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices — Create an invoice - **Operation ID**: `public-api.v1.invoices.create` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.create Create a sales invoice. It is created in `draft` by default; pass `options.issue_directly: true` to issue it immediately (assigning the correlative number and freezing the document per AEAT), or issue it later. VeriFactu *alta* is transmitted to AEAT asynchronously — a `201` does not mean AEAT has accepted the invoice yet, so poll it for the AEAT status. ```json { "client_id": "0190a1b2-c3d4-7e5f-8a90-1b2c3d4e5f60", "lines": [{ "description": "Consulting", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }], "options": { "issue_directly": true } } ``` Limits: at least one line is required; send an `Idempotency-Key` (≤255 chars, remembered 24 h) for safe retries; a duplicate `external_id` upserts the existing invoice instead of creating a new one. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 11 properties; 5 required: `client_id`, `series_id`, `issued_on`, `due_on`, `lines`. Create a sales invoice. Required: `client_id`, `series_id`, `issued_on`, `due_on` and `lines[]` (at least one). Optional: `notes`, `metadata`, `tags`, `custom_fields`, and an `options` object to atomically create, issue, send and wait for the PDF in a single call. Without `options` the invoice is created as a draft. A line may also be a DISBURSEMENT (`line_type: "SUPLIDO"`): an amount paid in the name and on behalf of the client (an official fee, duty or registry charge) that is re-invoiced at cost and, under art. 78.Tres.3 of the Spanish VAT Act (LIVA), stays out of the taxable base — it carries no VAT, withholding, surcharge, discount or product, requires `source_invoice_reference`, and is not allowed on a simplified (`F2`) invoice. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line returns `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. - `client_id` (string, required, format: uuid) - `series_id` (string, required, format: uuid) - `issued_on` (string, required, format: date) - `due_on` (string, required, format: date) - `notes` (string | null, optional, maxLength 1000) - `external_id` (string | null, optional, maxLength 100) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `options` (object, optional) - `issue_directly` (boolean | null, optional) - `send_automatically` (boolean | null, optional) - `send_to` (string | null, optional, format: email) - `wait_for_pdf` (boolean | null, optional) - `lines` (array, required) - `description` (string, required, maxLength 255) - `quantity` (number, required, min 0.01) - `unit_price` (number, required, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `regime_key` (string | null, optional, enum: `01`, `02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`, `14`, `15`, `17`, `18`, `19`, `20`) - `exemption_reason` (string | null, optional, enum: `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) - `line_type` (string | null, optional, enum: `NORMAL`, `SUPLIDO`) — Kind of line: `NORMAL` (default) for an ordinary line of your own operation, or `SUPLIDO` for a DISBURSEMENT — an amount you paid in the name and on behalf of the client (an official fee, duty or registry charge) and now re-invoice at cost. A disbursement is not part of your taxable base (art. 78.Tres.3 LIVA): it stays out of `subtotal`/`taxes_total`/`total`, is aggregated into `total_disbursements`, and is never declared in the AEAT VeriFactu record. A `SUPLIDO` line must carry no VAT, withholding, surcharge, discount, regime key, exemption cause or product, and is rejected on a simplified (`F2`) invoice. - `source_invoice_reference` (string | null, optional, maxLength 100) — Reference of the supporting document that originated the disbursement — the receipt or fee number issued by the public body (up to 100 characters). REQUIRED when `line_type` is `SUPLIDO`; leave it out on a normal line. Free text on purpose: the receipt of a public body is rarely registered as a purchase invoice. - `source_invoice_ids` (array | null, optional) — Optional traceability of a disbursement: list of IDs (UUID v7) of your own purchase invoices that back it. A purchase invoice of another company is rejected with 422. Omit it (or send `null`) when there is nothing to link. - `unit` (string | null, optional, maxLength 20) — Unit of measure printed next to the quantity on the document (`hours`, `kg`, `units`, …), up to 20 characters. Presentation only: free text, no closed catalog and no fiscal effect. - `exemption_reason_text` (string | null, optional, maxLength 255) — Free-text wording of the exemption provision of this line (up to 255 characters), printed under the line description to satisfy the mention required by art. 6.1.j of Royal Decree 1619/2012 when the catalogued cause does not cover it. Orthogonal to `exemption_reason` and to the document-level exemption cause; no coherence is enforced between them. - `line_total` (number | null, optional) — Optional CHECKSUM of the line total. When sent, it is compared against the total this API computes and the request is rejected with 422 (`line_total_checksum_mismatch`, with the expected and received values in `error.details`) when they differ by more than one cent. Never stored and never returned: the invoiced amount is always the computed one, so this field only reports a rounding mismatch with your own ERP. Omit it and no checksum runs. ## Responses - **201** — Invoice created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/create-recurring — Create a recurring invoice from an invoice - **Operation ID**: `public-api.v1.invoices.create_recurring` - **Tag**: Invoices - **Required scope**: `recurring_invoices:write` — Create and update recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.create_recurring Create a recurring invoice template that reuses the lines, client, and series of an existing invoice, applying the cadence (frequency, start date, optional end date and limits) supplied in the body. Returns the new recurring invoice. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 11 properties; 2 required: `frequency`, `start_on`. Create a recurring invoice from an existing invoice, reusing its lines, client and series. The body supplies only the recurrence configuration: `frequency` and `start_on` are required; `end_on`, `name`, `description`, `notes`, `metadata`, `holiday_handling`, `days_before_due`, `max_occurrences` and an `auto_delivery` object are optional. - `frequency` (string, required, enum: `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual`, `yearly`) - `start_on` (string, required, format: date) - `end_on` (string | null, optional, format: date) - `name` (string | null, optional, maxLength 255) - `description` (string | null, optional) - `notes` (string | null, optional) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `holiday_handling` (string | null, optional) - `days_before_due` (integer | null, optional, min 0) - `max_occurrences` (integer | null, optional, min 1) - `auto_delivery` (object, optional) - `send_automatically` (boolean | null, optional) - `recipients` (array | null, optional) - `cc` (array | null, optional) - `subject` (string | null, optional, maxLength 255) - `body` (string | null, optional, maxLength 5000) ## Responses - **201** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/invoices/{invoice} — Delete an invoice - **Operation ID**: `public-api.v1.invoices.delete` - **Tag**: Invoices - **Required scope**: `invoices:delete` — Delete invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.delete Delete a draft invoice. Issued invoices cannot be deleted (use `void` instead). ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/duplicate — Duplicate an invoice - **Operation ID**: `public-api.v1.invoices.duplicate` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.duplicate Create a new draft invoice by copying the lines, client, and metadata from an existing invoice. The new invoice gets a fresh `uuid` and number. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** — Invoice duplicated successfully. The `Location` header contains the canonical URL of the NEW invoice created. - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/export/excel — Export invoices to a spreadsheet - **Operation ID**: `public-api.v1.invoices.export_excel` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.export_excel Export a selection of invoices to a spreadsheet (`xlsx` or `csv`) and stream it back as an attachment. Narrow it with the filters (`invoice_ids[]`, `client_id`, `series_id`, `status`, `date_from`, `date_to`, `search`) or omit them to export everything. `format` picks the layout: `SUMMARY` (one row per invoice) or `ITEMS` (one row per line). ```http GET /v1/invoices/export?format=SUMMARY&status=paid&date_from=2026-01-01&date_to=2026-03-31 ``` Limits: the selection is capped at 5,000 invoices; a wider one returns 422 `export_limit_exceeded`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 9 properties; none of them required. Export invoices to a spreadsheet. `format` selects the content layout (`SUMMARY` one row per invoice, `ITEMS` one row per line) and `file_format` the file type (`xlsx` or `csv`). Narrow the selection with `date_from`/`date_to`, `client_id`, `series_id` or `invoice_ids`, or omit them to export everything. - `status` (string | null, optional, enum: `draft`, `scheduled`, `sent`, `paid`, `cancelled`, `overdue`, `annulled`) — Invoice status to filter by (draft, sent, paid, overdue, cancelled, annulled, scheduled). - `date_from` (string | null, optional, format: date-time) — Start of the issue-date range (inclusive). - `date_to` (string | null, optional, format: date-time) — End of the issue-date range (inclusive). - `client_id` (string | null, optional) — Client ID (UUID v7 value) to filter invoices by. - `series_id` (string | null, optional) — Series ID (UUID v7 value) to filter invoices by. - `search` (string | null, optional, maxLength 255) — Free-text search term (number, recipient, series). - `format` (string | null, optional, enum: `SUMMARY`, `ITEMS`) — Content layout: SUMMARY (one row per invoice) or ITEMS (one row per line). - `file_format` (string | null, optional, enum: `xlsx`, `csv`) — Formato de fichero: xlsx o csv. - `invoice_ids` (array | null, optional, maxItems 5000) — List of invoice IDs (UUID v7 values) to export. If omitted, exports the set matching the filters. ## Responses - **200** - Body (`application/json`): - object - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/find-by-external-id — Find an invoice by external ID - **Operation ID**: `public-api.v1.invoices.find_by_external_id` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.find_by_external_id Look up a single invoice by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Distinct from the fiscal number and the `uuid`. Returns the matching invoice or 404 `invoice_not_found` if no invoice uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up an invoice by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/find-by-number — Find an invoice by number - **Operation ID**: `public-api.v1.invoices.find_by_number` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.find_by_number Looks up a single invoice by its number, with optional `year` to disambiguate across fiscal years. Returns 404 if not found and 422 if the number is ambiguous and no `year` is supplied. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `number`. Public REST API v1 — POST /v1/invoices/find-by-number. Body: `number` (invoice number, required), `year` optional to disambiguate when the number repeats across fiscal years. Aligned with Supplier's `find-by-tax-id`. - `number` (string, required, maxLength 80) - `year` (integer | null, optional, min 2000, max 2100) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices — List all invoices - **Operation ID**: `public-api.v1.invoices.list` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.list List your sales invoices with cursor-based pagination. Supports filtering by `status[in]`, `client_id`, `series_id`, `issued_on[gte|lte]`, and `total[gte|lte]`. ## Query parameters - `original_invoice_id` (string | null, optional, format: uuid) - `verifactu_status` (string | null, optional, enum: `no_verifactu`, `pending`, `accepted`, `rejected`) - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Invoice status. - `status[in]` (string, optional) — Invoice status. - `client_id` (string, optional, format: uuid) — Client ID (UUID v7). - `client_id[in]` (string, optional) — Client ID (UUID v7). - `series_id` (string, optional, format: uuid) — Series ID (UUID v7). - `series_id[in]` (string, optional) — Series ID (UUID v7). - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `due_on[gte]` (string, optional, format: date) — Due date. - `due_on[lte]` (string, optional, format: date) — Due date. - `due_on[gt]` (string, optional, format: date) — Due date. - `due_on[lt]` (string, optional, format: date) — Due date. - `total[gte]` (number, optional) — Total amount. - `total[lte]` (number, optional) — Total amount. - `total[gt]` (number, optional) — Total amount. - `total[lt]` (number, optional) — Total amount. - `currency` (string, optional) — ISO 4217 currency code (e.g. EUR). - `currency[in]` (string, optional) — ISO 4217 currency code (e.g. EUR). - `number` (string, optional) — Invoice number. - `tags` (string, optional) — Filter by classification tag (lowercase slug). - `tags[in]` (string, optional) — Filter by classification tag (lowercase slug). - `sort` (string, optional, enum: `created`, `-created`, `total`, `-total`, `number`, `-number`) — Sort order. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/mark-paid — Mark invoice as paid - **Operation ID**: `public-api.v1.invoices.mark_paid` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.mark_paid Mark an invoice as fully paid. Idempotent: if already paid, returns the invoice unchanged. Returns 422 if the invoice is in a status that cannot transition to `paid`. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 4 properties; none of them required. - `paid_on` (string | null, optional, format: date) - `payment_method` (string | null, optional, maxLength 100) - `payment_reference` (string | null, optional, maxLength 191) - `notes` (string | null, optional, maxLength 1000) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/mark-sent — Mark an invoice as sent - **Operation ID**: `public-api.v1.invoices.mark_sent` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.mark_sent Transitions a draft invoice to `sent` without dispatching email. Useful when the document was delivered through an external channel. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/payment-receipt — Download payment receipt PDF - **Operation ID**: `public-api.v1.invoices.payment_receipt` - **Tag**: Invoices - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.payment_receipt Streams the PDF receipt of a paid invoice. Returns 422 if the invoice is not in `paid` status. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/pdf`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/payments — Register a payment - **Operation ID**: `public-api.v1.invoices.payments_create` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.payments_create Register a partial (or full) payment against an invoice. The invoice transitions to `partially_paid` while the cumulative paid amount is below the total, and to `paid` once it reaches it. Returns 422 if the invoice is in a status that does not accept payments. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 3 required: `amount`, `paid_on`, `payment_method`. Register a partial (or full) payment against a sales invoice. Required: `amount` (> 0), `paid_on` (date) and `payment_method` (a value from the closed catalog). Optional: `reference`, `notes`. - `amount` (number, required, min 0.01) - `paid_on` (string, required, format: date) - `payment_method` (string, required, enum: `bank_transfer`, `direct_debit`, `sepa_direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `bizum`, `other`) - `reference` (string | null, optional, maxLength 255) - `notes` (string | null, optional, maxLength 1000) ## Responses - **201** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/payments — List invoice payments - **Operation ID**: `public-api.v1.invoices.payments_list` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.payments_list List the payments registered against an invoice, ordered by payment date. Returns an empty array when no payments have been registered yet. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/pdf — Download invoice PDF - **Operation ID**: `public-api.v1.invoices.pdf` - **Tag**: Invoices - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.pdf Download the PDF representation of an invoice. Returns the binary PDF stream (`application/pdf`). ## Path parameters - `invoice` (string, required) ## Query parameters - `download` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/pdf`): - string - **304** - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/pdf-link — Generate temporary PDF link - **Operation ID**: `public-api.v1.invoices.pdf_link` - **Tag**: Invoices - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.pdf_link Returns a temporary URL to the invoice PDF instead of streaming the bytes. Convenient for embedding in emails or messaging apps. Dual contract: 200 with the URL when the PDF is already materialized; 202 with `status: pendiente` when generation was enqueued (the PDF renders on the `pdf` queue) — retry until you get the 200. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — The PDF is already materialized: returns a temporary `url` to download it, its `filename` and the `expires_at` of the link. - Body (`application/json`): - `data` (object, required) - `url` (string, required) - `filename` (string, required) - `expires_at` (string, required) - **202** — The PDF has not been generated yet: its generation is enqueued and the response reports the `pendiente` status. Retry shortly to obtain the link (200). - Body (`application/json`): - `data` (object, required) - `status` (string, required, const: `pendiente`) - `document_uuid` (string, required) - `pdf_path` (string, required) - `pdf_url` (string, required) - `message` (string, required, const: `La generación del PDF está en curso. El enlace estará disponible en unos segundos.`) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/pdf/preview — Preview an invoice draft PDF - **Operation ID**: `public-api.v1.invoices.pdf_preview` - **Tag**: Invoices - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.pdf_preview Stream a non-fiscal draft PDF (`application/pdf`) of an invoice marked BORRADOR, with a placeholder number and no VeriFactu QR. Nothing is persisted: the series counter and fingerprint are untouched. Returns 422 for an already issued invoice — use the standard `pdf` endpoint instead. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/public-link — Retrieve invoice public link - **Operation ID**: `public-api.v1.invoices.public_link_get` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.public_link_get Returns the shareable public URL of the invoice (/d/{uuid}) along with its status, expiration, and the plan-allowed maximum extension days. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/invoices/{invoice}/public-link — Update invoice public link - **Operation ID**: `public-api.v1.invoices.public_link_update` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.public_link_update Applies an action to the public link: `revoke`, `activate`, `extend` (with `extend_days`), or `reset` to the plan default. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `action`. Public REST API v1 — PUT /v1/invoices/{uuid}/public-link. `SchemaName` sets a unique, clean OpenAPI schema name (`UpdateInvoicePublicLinkRequest`), consistent with Quote/Proforma/DeliveryNote for the same operation, without renaming the class. - `action` (string, required, enum: `revoke`, `activate`, `extend`, `reset`) - `extend_days` (integer, optional, min 1, max 36500) ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/quarterly/available-quarters — List quarters with invoices - **Operation ID**: `public-api.v1.invoices.quarterly.available` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.quarterly.available Returns the quarters that have at least one invoice, with breakdown by invoice type (F1/F2/F3/R5). Useful to populate "quarter to export" selectors. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/quarterly/download-zip — Generate quarterly ZIP archive - **Operation ID**: `public-api.v1.invoices.quarterly.download_zip` - **Tag**: Invoices - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.quarterly.download_zip Builds a ZIP with all invoice PDFs of the given quarter. Returns ZIP metadata (path, processed counts, errors). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 2 required: `year`, `quarter`. Public REST API v1 — POST /v1/invoices/quarterly/download-zip and POST /v1/invoices/quarterly/send-email (shared fields). The canonical recipient field is `email`. We accept `to_email` as a deprecated alias for compatibility with older clients. - `year` (integer, required, min 2000, max 2027) - `quarter` (integer, required, min 1, max 4) - `include_index` (boolean | null, optional) - `email` (string | null, optional, format: email) - `message` (string | null, optional, maxLength 5000) ## Responses - **200** - Body (`application/zip`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/quarterly/send-email — Email quarterly ZIP to accountant - **Operation ID**: `public-api.v1.invoices.quarterly.send_email` - **Tag**: Invoices - **Required scope**: `invoices:send` — Send by email invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.quarterly.send_email Generates the quarterly ZIP and emails it to the recipient, typically the tax accountant. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 2 required: `year`, `quarter`. Public REST API v1 — POST /v1/invoices/quarterly/download-zip and POST /v1/invoices/quarterly/send-email (shared fields). The canonical recipient field is `email`. We accept `to_email` as a deprecated alias for compatibility with older clients. - `year` (integer, required, min 2000, max 2027) - `quarter` (integer, required, min 1, max 4) - `include_index` (boolean | null, optional) - `email` (string | null, optional, format: email) - `message` (string | null, optional, maxLength 5000) ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `message` (string, required, const: `Email trimestral encolado correctamente.`) - `year` (integer, required) - `quarter` (integer, required) - `recipient` (any, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/reminder-preview — Preview a payment reminder email - **Operation ID**: `public-api.v1.invoices.reminder_preview` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.reminder_preview Renders the HTML, subject, and resolved recipients of the reminder email without sending it. Same override fields as send-reminder. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 7 properties; none of them required. Public REST API v1 — POST /v1/invoices/{uuid}/send-reminder and POST /v1/invoices/{uuid}/reminder-preview (same fields). All fields are optional — without overrides the handler uses the canonical payment reminder template. - `email` (string | null, optional, format: email) — Recipient other than the client email. - `subject` (string | null, optional, maxLength 200) — Custom subject for the reminder. - `message` (string | null, optional, maxLength 5000) — Additional message for the reminder body. - `attach_pdf` (boolean | null, optional) — Per-send override for "attach the invoice PDF". If omitted (or `null`), the company default (`email_settings`) is used. - `stripe_payment_button` (boolean | null, optional) — Per-send override for the "Stripe payment button". If omitted (or `null`), the company default is used. - `cc` (array | null, optional) — Direcciones en copia. - `bcc` (array | null, optional) — Direcciones en copia oculta. ## Responses - **200** - Body (`application/json`): - `data` (object (InvoiceReminderPreview), required) — Preview of the payment reminder email (subject + HTML + resolved recipients) without sending it. Useful to show exactly what would be sent before triggering `POST .../send-reminder`. - `subject` (string, required) — Subject of the reminder email. - `html` (string, required) — Rendered HTML body of the email. - `from` (string, required, format: email) — Sender address. - `from_name` (string, required) — Human-readable sender name. - `to` (string, required, format: email) — Destinatario principal resuelto. - `cc` (array, required) — Carbon copy addresses. Empty `[]` when there are none. - `bcc` (array, required) — Blind carbon copy addresses. Empty `[]` when there are none. - `status` (string, required) — Current status of the invoice (e.g. `sent`, `overdue`). - `invoice_number` (string, required) — Invoice number. - `total` (number, required) — Total amount of the invoice. - `due_date` (string | null, required, format: date) — Due date (YYYY-MM-DD), or `null` if not applicable. - `public_url` (string | null, required, format: uri) — Public link to the invoice, or `null` if the link is not active. - `public_link_active` (boolean, required) — Indicates whether the public link is active. - `public_link_expires_at` (string | null, required, format: date-time) — When the public link expires (ISO 8601), or `null`. - `reminders_sent` (integer, required) — Number of reminders already sent for this invoice. - `last_reminder_sent_at` (string | null, required, format: date-time) — When the last reminder was sent (ISO 8601), or `null` if none has been sent. - `cooldown_active` (boolean, required) — Indicates whether there is an active cooldown period that prevents sending another reminder yet. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PATCH /v1/invoices/{invoice}/reschedule — Reschedule an invoice - **Operation ID**: `public-api.v1.invoices.reschedule` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.reschedule Move the issuance date of an already scheduled invoice. The invoice **stays `scheduled` throughout** — unlike `unschedule` followed by `schedule`, it never returns to `draft`, so it is never editable or deletable in between and there is no window in which the sweep could find it unscheduled. **What you can change:** `scheduled_for`, and only that. `scheduled_action` is preserved — a schedule created as `issue_and_send` still emails the client at the new date, and one created as `draft` still does not. To change the action you have to `unschedule` and `schedule` again. The content of the invoice (lines, client, series, totals) is untouched by this call: use `PATCH /v1/invoices/{id}` while it is still a draft for that. Limits: only an invoice in `scheduled` can be rescheduled — a `draft` (never scheduled) or an already issued invoice returns 422 — and the new `scheduled_for` must be strictly in the future (422 otherwise). Everything documented under `schedule` about what happens when the date arrives (number assigned at that moment, snapshots frozen, asynchronous VeriFactu *alta*, email only with `issue_and_send`, per-invoice retry on failure) applies unchanged to the new date. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `scheduled_for`. Reschedule an already `scheduled` invoice. Required: `scheduled_for` (ISO 8601 date-time, strictly in the future). - `scheduled_for` (string, required, format: date-time) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/schedule — Schedule an invoice - **Operation ID**: `public-api.v1.invoices.schedule` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.schedule Reserve the issuance of a draft invoice for a future instant. The invoice moves to `scheduled` and **nothing fiscal happens yet**: it keeps its `BORRADOR` placeholder number, no series counter is consumed and nothing is registered with VeriFactu. Scheduling never burns numbering. **What happens at `scheduled_for`.** A sweep runs every minute and, on the first pass at or after that instant, it: (1) assigns the definitive correlative number of the series **at that moment**, not when you scheduled — so a document scheduled today and issued next month takes the number that corresponds to next month; (2) freezes the recipient and issuer snapshots as of that instant, which is what the PDF and the fiscal XML will show; (3) moves the invoice to `sent`; (4) queues the VeriFactu *alta* to AEAT **asynchronously** when the company is enrolled; and (5) emails the client **only** when `scheduled_action` is `issue_and_send` and the client has an email on file — with `scheduled_action: draft` the invoice is issued but never delivered, and `issue_and_send` without a recipient email still issues it, silently skipping the delivery. **Time zone.** `scheduled_for` is an ISO 8601 date-time. If it carries an explicit offset (`2027-01-15T09:00:00Z`, `…+01:00`) that offset is honoured; without one it is read in the account's server time zone, `Europe/Madrid`. Resolution is minute-level: expect issuance within about a minute of the instant you asked for, never before it. **If the scheduled issuance fails**, each invoice is isolated in its own transaction: the failing one stays `scheduled` with its date in the past, the error is logged, the rest of the batch is unaffected and the next sweep retries it. A successful issuance is never repeated, because `scheduled → sent` can only happen once. Limits: only a `draft` can be scheduled (any other status returns 422) and `scheduled_for` must be strictly in the future (422 otherwise). While it is still `scheduled` you can call `unschedule` to return it to `draft`, or `reschedule` to move only the date. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `scheduled_for`, `scheduled_action`. Schedule the future issuance of a `draft` invoice. Required: `scheduled_for` (ISO 8601 date-time, strictly in the future) and `scheduled_action` (`draft` or `issue_and_send`). - `scheduled_for` (string, required, format: date-time) - `scheduled_action` (string, required, enum: `draft`, `issue_and_send`) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/send — Send invoice by email - **Operation ID**: `public-api.v1.invoices.send` - **Tag**: Invoices - **Required scope**: `invoices:send` — Send by email invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.send Send an invoice to the client by email. Uses the email on file unless overridden in the payload. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 5 properties; none of them required. Public REST API v1 — POST /v1/invoices/{uuid}/send. Optional body: `to` (string), `cc[]`, `bcc[]` (arrays of emails), `subject` (max 200), `body` (string). The controller performs the cross-field validation: if the client has no email and `to` is absent, it returns 422 `missing_required_param`. - `to` (string | null, optional, format: email, maxLength 191) - `subject` (string | null, optional, maxLength 200) - `body` (string | null, optional, maxLength 5000) - `cc` (array | null, optional) - `bcc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/send-reminder — Send a payment reminder - **Operation ID**: `public-api.v1.invoices.send_reminder` - **Tag**: Invoices - **Required scope**: `invoices:send` — Send by email invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.send_reminder Emails a payment reminder to the customer for this invoice. Accepts optional `email`, `subject`, `message`, `cc`, `bcc` overrides. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 7 properties; none of them required. Public REST API v1 — POST /v1/invoices/{uuid}/send-reminder and POST /v1/invoices/{uuid}/reminder-preview (same fields). All fields are optional — without overrides the handler uses the canonical payment reminder template. - `email` (string | null, optional, format: email) — Recipient other than the client email. - `subject` (string | null, optional, maxLength 200) — Custom subject for the reminder. - `message` (string | null, optional, maxLength 5000) — Additional message for the reminder body. - `attach_pdf` (boolean | null, optional) — Per-send override for "attach the invoice PDF". If omitted (or `null`), the company default (`email_settings`) is used. - `stripe_payment_button` (boolean | null, optional) — Per-send override for the "Stripe payment button". If omitted (or `null`), the company default is used. - `cc` (array | null, optional) — Direcciones en copia. - `bcc` (array | null, optional) — Direcciones en copia oculta. ## Responses - **200** - Body (`application/json`): - `data` (object (InvoiceReminderSent), required) — Acknowledgement of sending a payment reminder. - `id` (string, required) — UUID (v7) of the invoice the reminder was sent to. - `message` (string, required, const: `Recordatorio enviado correctamente.`) — Confirmation message. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice} — Retrieve an invoice - **Operation ID**: `public-api.v1.invoices.show` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.show Retrieve a sales invoice by its `uuid`. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/simplified-eligibility — Check simplified invoice eligibility - **Operation ID**: `public-api.v1.invoices.simplified_eligibility` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.simplified_eligibility Determines if an invoice may be issued as simplified (F2) under Real Decreto 1619/2012 art. 4 based on amount and counterparty data. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 1 required: `total`. Public REST API v1 — POST /v1/invoices/simplified-eligibility. Checks whether an invoice with a given `total` amount (and, optionally, client data) can be issued as simplified (F2) or must be issued as a full invoice (F1) under Royal Decree 1619/2012 (art. 4). - `total` (number, required, min 0) - `client_id` (string | null, optional, format: uuid) - `client_country` (string | null, optional, maxLength 2, minLength 2) - `is_intra_community` (boolean | null, optional) - `is_reverse_charge` (boolean | null, optional) - `client_requires_deductible` (boolean | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (SimplifiedInvoiceEligibility), required) — Result of checking whether an invoice can be issued as simplified (F2) under RD 1619/2012 art. 4. - `can_be_simplified` (boolean, required) — Whether the invoice can be issued as a simplified invoice (F2). - `must_be_complete` (boolean, required) — Whether the invoice must be issued as a full invoice (F1). It is the complement of `can_be_simplified`. - `reason_code` (string, required) — Code of the blocking reason when it cannot be simplified (`intra_community`, `reverse_charge`, `export_operation`, `client_deduction_required`, `over_absolute_limit`). Empty string `""` when it can be simplified. - `reason_message` (string, required) — Human-readable (Spanish) message of the blocking reason. Empty string `""` when it can be simplified. - `sector_limit` (number, required) — Absolute legal limit (EUR) above which a simplified invoice is not allowed. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/stats — Get invoice statistics - **Operation ID**: `public-api.v1.invoices.stats` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.stats Returns aggregate KPIs for the company: counts by status, revenue, pending and overdue totals, average days to payment, and corrective counts. Filterable by period (defaults to the current year). ## Query parameters - `date_from` (string | null, optional, format: date) - `date_to` (string | null, optional, format: date) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (InvoiceStats), required) — Aggregated invoicing metrics (KPIs) for the queried period. - `object` (string, required, enum: `invoice_stats`) - `total_count` (integer, required) — Total number of invoices in the period. - `by_status` (object, required) — Invoice count by status (key = status, value = number of invoices). - `revenue_total` (number, required) — Total amount invoiced in the period. - `pending_amount` (number, required) — Total amount pending collection. - `overdue_count` (integer, required) — Number of overdue and unpaid invoices. - `overdue_amount` (number, required) — Total amount overdue and unpaid. - `average_payment_days` (number | null, required) — Average number of days until payment of paid invoices. `null` if there are no paid invoices in the period. - `corrective_count` (integer, required) — Number of corrective invoices issued in the period. - `period` (object, required) — Period covered by the metrics. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/statuses — List invoice statuses - **Operation ID**: `public-api.v1.invoices.statuses` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.statuses Lists the closed catalog of invoice statuses with their public `value`, localized `label`, and UI `color`. Use it to populate filters or status pickers instead of hard-coding values. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `value` (string, required, enum: `draft`, `sent`, `paid`, `cancelled`, `overdue`, `annulled`) — Internal status identifier. - `label` (string, required) — Human-readable status label (Spanish). - `color` (string, required) — Suggested color for rendering the status in the UI. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/substitute-simplified — Substitute simplified invoices with full invoice - **Operation ID**: `public-api.v1.invoices.substitute_simplified` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.substitute_simplified Groups N simplified invoices (F2) under a single substitutive full invoice (F3) with complete recipient data. Marks the originals as substituted. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `client_id`, `simplified_invoice_ids`. - `client_id` (string, required, format: uuid) - `notes` (string | null, optional, maxLength 2000) - `simplified_invoice_ids` (array, required) ## Responses - **201** — Full invoice (F3) created as a replacement for the simplified one. The `Location` header contains the canonical URL of the NEW invoice. - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/unschedule — Unschedule an invoice - **Operation ID**: `public-api.v1.invoices.unschedule` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.unschedule Cancel a scheduled issuance. The invoice returns to `draft`, `scheduled_for` and `scheduled_action` are set back to `null`, and it becomes editable and deletable again as any other draft. Unscheduling leaves **no fiscal trace**, because nothing fiscal had happened yet: no correlative number of the series was consumed (the invoice still carries its `BORRADOR` placeholder), nothing was registered with VeriFactu and no email was sent. This is not an annulment and it does not appear in any AEAT record. **Window of use.** It only applies while the invoice is `scheduled`. A `draft` that was never scheduled returns 422, and so does an invoice the sweep has already issued: from that instant on it is `sent`, it owns a definitive number and — where VeriFactu applies — an AEAT record, so the way back is no longer `unschedule` but `void`/`annul` to withdraw it (only while it is unpaid) or `corrective` to amend it. In practice the race is real: an invoice whose `scheduled_for` has just elapsed may already have been issued when your call lands. If you only want to move the date, use `PATCH /v1/invoices/{id}/reschedule` instead — it avoids the round trip through `draft` and the window in which the document is editable. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/unsend — Unsend an invoice - **Operation ID**: `public-api.v1.invoices.unsend` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.unsend Clear the delivery marker (`sent_at`) of a `sent` invoice while keeping its `sent` status. The correlative number and VeriFactu record stay intact — the invoice is not reverted to draft and remains immutable per AEAT. Use it to undo an accidental mark-as-sent. Idempotent: a no-op when `sent_at` is already null. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/invoices/{invoice} — Update an invoice - **Operation ID**: `public-api.v1.invoices.update` - **Tag**: Invoices - **Required scope**: `invoices:write` — Create and update invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.update Update a draft invoice. Once an invoice has been issued (status `issued`), most fields become immutable per AEAT compliance. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 9 properties; none of them required. - `client_id` (string, optional, format: uuid) - `issued_on` (string, optional, format: date) - `due_on` (string, optional, format: date) - `notes` (string | null, optional, maxLength 1000) - `external_id` (string | null, optional, maxLength 100) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional, min 0.01) - `unit_price` (number, optional, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `regime_key` (string | null, optional, enum: `01`, `02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`, `14`, `15`, `17`, `18`, `19`, `20`) - `exemption_reason` (string | null, optional, enum: `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) - `line_type` (string | null, optional, enum: `NORMAL`, `SUPLIDO`) — Kind of line: `NORMAL` (default) for an ordinary line of your own operation, or `SUPLIDO` for a DISBURSEMENT — an amount you paid in the name and on behalf of the client and now re-invoice at cost, which stays out of the taxable base (art. 78.Tres.3 LIVA). Replacing the lines of a draft re-applies the same rules, so keep the field when you resend a line you read from the invoice: sending it as `NORMAL` (or omitting it) turns the disbursement into an ordinary taxable line and changes the invoice amount. - `source_invoice_reference` (string | null, optional, maxLength 100) — Reference of the supporting document that originated the disbursement — the receipt or fee number issued by the public body (up to 100 characters). REQUIRED when `line_type` is `SUPLIDO`; leave it out on a normal line. - `source_invoice_ids` (array | null, optional) — Optional traceability of a disbursement: list of IDs (UUID v7) of your own purchase invoices that back it. A purchase invoice of another company is rejected with 422. - `unit` (string | null, optional, maxLength 20) — Unit of measure printed next to the quantity on the document (`hours`, `kg`, `units`, …), up to 20 characters. Presentation only: free text, no closed catalog and no fiscal effect. - `exemption_reason_text` (string | null, optional, maxLength 255) — Free-text wording of the exemption provision of this line (up to 255 characters), printed under the line description to satisfy the mention required by art. 6.1.j of Royal Decree 1619/2012 when the catalogued cause does not cover it. - `line_total` (number | null, optional) — Optional CHECKSUM of the line total. When sent, it is compared against the total this API computes and the request is rejected with 422 (`line_total_checksum_mismatch`, with the expected and received values in `error.details`) when they differ by more than one cent. Never stored and never returned. ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/void — Void an invoice - **Operation ID**: `public-api.v1.invoices.void` - **Tag**: Invoices - **Required scope**: `invoices:void` — Void invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.invoices.void Withdraw an issued invoice. The invoice moves to `annulled`, `voided_at` starts reporting when that happened and the status is terminal: voiding is **irreversible** and there is no way back to `sent` or `draft`. **Void or correct?** Void when the whole document should never have existed and has not been paid — the invoice is withdrawn as a whole and no amending document is produced. Issue a corrective (`POST /v1/invoices/{id}/corrective`) when the invoice was already paid, or when only part of it is wrong (amount, recipient, partial return): a `paid` invoice can never be voided, and voiding never fixes a figure. What voiding does **not** do: the correlative number of the series is neither released nor reused (the series counter only moves forward), the original invoice is not deleted, and its VeriFactu *alta* record is not withdrawn. When the company is enrolled in VeriFactu, an AEAT cancellation (*anulación*) record is queued **asynchronously** with your `reason` as its `motivo` — a `200` means the invoice is annulled on our side, not that AEAT has already processed the cancellation. With VeriFactu inactive the annulment is purely internal. Limits: only an invoice in `sent` or `overdue` can be voided. A `draft` is not voidable (delete it instead), and `paid`, `cancelled` and `annulled` return 422. An invoice that **is** a corrective can never be voided — to undo a wrong corrective, issue a new corrective of the original. Note the inverse is allowed: having correctives does not block voiding the original. Call `GET /v1/invoices/{id}/can-annul` first if you need to check eligibility without attempting the change. `reason` is optional here and a placeholder is persisted when you omit it. `POST /v1/invoices/{id}/annul` is the very same operation with `reason` required — prefer it whenever the reason must be documented. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. Public REST API v1 — POST /v1/invoices/{uuid}/void. Optional body: `reason` (string). If the API client does not send a reason, a placeholder is persisted ("Anulada via API v1."). - `reason` (string | null, optional, maxLength 500) ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **422** — Validation failed, or the invoice cannot undergo the requested state transition (e.g. marking an already-paid invoice as paid, or editing an issued invoice — use a corrective instead). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/payment-methods — List payment methods - **Operation ID**: `public-api.v1.payment_methods.list` - **Tag**: Invoices - **Required scope**: `invoices:read` — Read invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/invoices/public-api.v1.payment_methods.list Lists the closed catalog of payment methods with their public `value` and localized `label`. Use it to populate the `payment_method` field when registering a payment instead of hard-coding values. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/monthly-time-record-closes — Close a monthly time record - **Operation ID**: `public-api.v1.monthly_time_record_closes.create` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.create Freeze the immutable monthly close of the time record register for a finished `(year, month)`: it snapshots each active employee’s balance totals and absence breakdown/balances (reusing the balance contract, never recomputing) and locks the period against retroactive entries and corrections. `year` and `month` (1-12) are required. A month that has not ended yet returns 422 in Spanish; a period already closed returns 409. Reopening a previously reopened period re-closes it, keeping its original `id`. Returns 201 with the created close and a `Location` header. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `year`, `month`. - `year` (integer, required, min 2000, max 2100) — Year of the period to close (YYYY). - `month` (integer, required, min 1, max 12) — Month of the period to close (1-12). ## Responses - **201** - Body (`application/json`): - `data` (object (MonthlyTimeRecordClose), required) — An immutable monthly close of the time record register for the Control Horario (time tracking) module. Closing a finished `(year, month)` freezes each active employee’s balance snapshot and locks the period against retroactive entries and corrections. The close can be reopened for an audited correction (state `closed ⇄ reopened`). - `id` (string, required, format: uuid) — Opaque identifier of the monthly close, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `monthly_time_record_close`) — Always `monthly_time_record_close`. - `year` (integer, required) — Year of the closed period. - `month` (integer, required, min 1, max 12) — Month of the closed period (1-12). - `status` (string, required, enum: `closed`, `reopened`) — State of the close: `closed` (immutable, period locked) or `reopened` (writes re-enabled for a correction). - `period_start` (string, required, format: date) — First day of the closed period (YYYY-MM-DD). - `period_end` (string, required, format: date) — Last day of the closed period (YYYY-MM-DD). - `closed_by_id` (string, required, format: uuid) — UUID v7 of the user who closed the period. - `closed_at` (string, required, format: date-time) — When the period was closed (ISO 8601). - `reopened_by_id` (string | null, required, format: uuid) — UUID v7 of the user who reopened the close; `null` while it is still `closed`. - `reopened_at` (string | null, required, format: date-time) — When the close was reopened (ISO 8601); `null` while it is still `closed`. - `employee_count` (integer, required) — Number of employees included in the close. - `is_sealed` (boolean, required) — Whether a digital seal exists for this close (mere presence; integrity verification is served by `GET /monthly-time-record-closes/{id}/seal`). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/monthly-time-record-closes/{monthly_time_record_close}/export — Download the closed register (RD-ley 8/2019) - **Operation ID**: `public-api.v1.monthly_time_record_closes.export` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.export Download the daily time record of a closed period as a spreadsheet in the `rdley_8_2019` format, read from the locked, tamper-evident ledger (append-only entries + hash chain) of the period. `format` is optional and defaults to `rdley_8_2019`; a format outside the catalog returns 422. A period without a close returns 404. The response is a binary file download. ## Path parameters - `monthly_time_record_close` (string, required) ## Query parameters - `format` (string | null, optional, enum: `rdley_8_2019`) — ITSS export format (defaults to rdley_8_2019). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - object - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/monthly-time-record-closes — List all monthly time record closes - **Operation ID**: `public-api.v1.monthly_time_record_closes.list` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.list List the monthly closes of the time record register of your company with cursor-based pagination, ordered by period descending. Supports filtering by `year`. Each item exposes its status (`closed`/`reopened`), period bounds and employee count. ## Query parameters - `per_page` (integer, optional, min 1, max 100, default: `25`) — Page size. - `limit` (integer, optional, min 1, max 100, default: `25`) — Page size. - `cursor` (string, optional) — Opaque pagination cursor. - `year` (integer, optional) — Filter monthly closes by calendar year. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the monthly close, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `monthly_time_record_close`) — Always `monthly_time_record_close`. - `year` (integer, required) — Year of the closed period. - `month` (integer, required, min 1, max 12) — Month of the closed period (1-12). - `status` (string, required, enum: `closed`, `reopened`) — State of the close: `closed` (immutable, period locked) or `reopened` (writes re-enabled for a correction). - `period_start` (string, required, format: date) — First day of the closed period (YYYY-MM-DD). - `period_end` (string, required, format: date) — Last day of the closed period (YYYY-MM-DD). - `closed_by_id` (string, required, format: uuid) — UUID v7 of the user who closed the period. - `closed_at` (string, required, format: date-time) — When the period was closed (ISO 8601). - `reopened_by_id` (string | null, required, format: uuid) — UUID v7 of the user who reopened the close; `null` while it is still `closed`. - `reopened_at` (string | null, required, format: date-time) — When the close was reopened (ISO 8601); `null` while it is still `closed`. - `employee_count` (integer, required) — Number of employees included in the close. - `is_sealed` (boolean, required) — Whether a digital seal exists for this close (mere presence; integrity verification is served by `GET /monthly-time-record-closes/{id}/seal`). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/monthly-time-record-closes/{monthly_time_record_close}/reopen — Reopen a monthly time record close - **Operation ID**: `public-api.v1.monthly_time_record_closes.reopen` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.reopen Reopen a `closed` monthly close by its `id` (UUID v7) — an audited recovery of an erroneous close that re-enables writes for the period. The close keeps its `id`; its status becomes `reopened`. A close that cannot be reopened returns 422 in Spanish, and one belonging to another company returns 404. Returns 200 with the reopened close. ## Path parameters - `monthly_time_record_close` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (MonthlyTimeRecordClose), required) — An immutable monthly close of the time record register for the Control Horario (time tracking) module. Closing a finished `(year, month)` freezes each active employee’s balance snapshot and locks the period against retroactive entries and corrections. The close can be reopened for an audited correction (state `closed ⇄ reopened`). - `id` (string, required, format: uuid) — Opaque identifier of the monthly close, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `monthly_time_record_close`) — Always `monthly_time_record_close`. - `year` (integer, required) — Year of the closed period. - `month` (integer, required, min 1, max 12) — Month of the closed period (1-12). - `status` (string, required, enum: `closed`, `reopened`) — State of the close: `closed` (immutable, period locked) or `reopened` (writes re-enabled for a correction). - `period_start` (string, required, format: date) — First day of the closed period (YYYY-MM-DD). - `period_end` (string, required, format: date) — Last day of the closed period (YYYY-MM-DD). - `closed_by_id` (string, required, format: uuid) — UUID v7 of the user who closed the period. - `closed_at` (string, required, format: date-time) — When the period was closed (ISO 8601). - `reopened_by_id` (string | null, required, format: uuid) — UUID v7 of the user who reopened the close; `null` while it is still `closed`. - `reopened_at` (string | null, required, format: date-time) — When the close was reopened (ISO 8601); `null` while it is still `closed`. - `employee_count` (integer, required) — Number of employees included in the close. - `is_sealed` (boolean, required) — Whether a digital seal exists for this close (mere presence; integrity verification is served by `GET /monthly-time-record-closes/{id}/seal`). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/monthly-time-record-closes/{monthly_time_record_close}/report — Retrieve the report of a closed period - **Operation ID**: `public-api.v1.monthly_time_record_closes.report` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.report Return the monthly report of a closed period by the close `id` (UUID v7), read from the frozen snapshot without recomputation, so the totals never drift from the sheet at the moment of closing. It holds the company aggregate totals and one row per employee with totals, absence breakdown and balances, and the daily detail. Totals are in minutes. A period without a close returns 404. A computed resource: it exposes `close_id`, never an `id` of its own. ## Path parameters - `monthly_time_record_close` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (MonthlyCloseReport), required) — The monthly report over a closed period for the Control Horario (time tracking) module. A computed resource with no entity identity: it is composed from the frozen snapshot of the close, so it references the close by `close_id` (UUID v7) and never an `id` of its own. The totals never drift from the sheet at the moment of closing. Totals are in minutes. - `object` (string, required, enum: `monthly_close_report`) — Always `monthly_close_report`. - `close_id` (string, required, format: uuid) — UUID v7 of the monthly close the report belongs to. - `year` (integer, required) — Year of the closed period. - `month` (integer, required, min 1, max 12) — Month of the closed period (1-12). - `status` (string, required, enum: `closed`, `reopened`) — State of the close (`closed`/`reopened`), so a reopened close is not mistaken for immutable. - `total_expected_minutes` (integer, required) — Company aggregate expected minutes in the period. - `total_worked_minutes` (integer, required) — Company aggregate worked minutes in the period. - `total_balance_minutes` (integer, required) — Company aggregate balance in the period (worked − expected). - `total_overtime_minutes` (integer, required) — Company aggregate overtime minutes in the period. - `employees` (array, required) — One row per employee of the closed period. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/monthly-time-record-closes/{monthly_time_record_close}/seal — Seal a monthly time record register - **Operation ID**: `public-api.v1.monthly_time_record_closes.seal` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.seal Seal (digitally sign) a `closed` monthly time record register by the close `id` (UUID v7): it freezes a canonical SHA-256 digest of the close snapshot and a detached RSA-SHA256 signature made with the company certificate, so the register is tamper-evident and independently verifiable. A close that is not `closed` returns 422 in Spanish, a period already sealed returns 409 (one seal per close, no re-sealing), and a company without an active usable certificate returns 422. A close belonging to another company returns 404. Returns 201 with the seal (including its live verification state) and a `Location` header. ## Path parameters - `monthly_time_record_close` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** - Body (`application/json`): - `data` (object (MonthlyRegisterSignature), required) — The digital seal (signature) of a closed monthly time record register for the Control Horario (time tracking) module. Sealing a `closed` period freezes a canonical SHA-256 digest of its snapshot and a detached RSA-SHA256 signature made with the company certificate, so the register is tamper-evident and independently verifiable by a third party (labour inspectorate, auditor). There is exactly one seal per close, and it is not re-sealable. The `verified`/`verification_reason` fields are recomputed live on every read against the current snapshot: reopening and re-closing the period with different data yields `verified: false` with reason `snapshot_mismatch`. - `id` (string, required, format: uuid) — Opaque identifier of the seal, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `monthly_register_signature`) — Always `monthly_register_signature`. - `monthly_time_record_close_id` (string, required, format: uuid) — UUID v7 of the monthly close this seal belongs to. - `digest` (object, required) — The canonical digest of the close snapshot at sealing time. - `signature` (object, required) — The detached digital signature of the digest. - `certificate` (object, required) — The company certificate the seal was made with, kept for traceability. - `sealed_by_id` (string, required, format: uuid) — UUID v7 of the user who sealed the register. - `sealed_at` (string, required, format: date-time) — When the register was sealed (ISO 8601). - `verified` (boolean, required) — Live verification: `true` if the current snapshot and the signature are intact. - `verification_reason` (string, required, enum: `verified`, `snapshot_mismatch`, `signature_invalid`, `certificate_unreadable`) — Reason of the live verification: `verified` (intact), `snapshot_mismatch` (the snapshot changed since sealing), `signature_invalid` (the signature no longer validates) or `certificate_unreadable` (the certificate could not be read). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/monthly-time-record-closes/{monthly_time_record_close}/seal — Retrieve the seal of a monthly register - **Operation ID**: `public-api.v1.monthly_time_record_closes.seal_show` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.seal_show Retrieve the digital seal of a monthly time record register by the close `id` (UUID v7), together with its verification state recomputed live against the current snapshot: `verified` is `true` when the snapshot and the signature are intact, otherwise `verification_reason` explains the mismatch (`snapshot_mismatch`, `signature_invalid` or `certificate_unreadable`). The seal exposes its digest, signature and signing certificate so a third party can verify it. A close without a seal — or belonging to another company — returns 404 `monthly_register_signature_not_found` (anti-enumeration). ## Path parameters - `monthly_time_record_close` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (MonthlyRegisterSignature), required) — The digital seal (signature) of a closed monthly time record register for the Control Horario (time tracking) module. Sealing a `closed` period freezes a canonical SHA-256 digest of its snapshot and a detached RSA-SHA256 signature made with the company certificate, so the register is tamper-evident and independently verifiable by a third party (labour inspectorate, auditor). There is exactly one seal per close, and it is not re-sealable. The `verified`/`verification_reason` fields are recomputed live on every read against the current snapshot: reopening and re-closing the period with different data yields `verified: false` with reason `snapshot_mismatch`. - `id` (string, required, format: uuid) — Opaque identifier of the seal, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `monthly_register_signature`) — Always `monthly_register_signature`. - `monthly_time_record_close_id` (string, required, format: uuid) — UUID v7 of the monthly close this seal belongs to. - `digest` (object, required) — The canonical digest of the close snapshot at sealing time. - `signature` (object, required) — The detached digital signature of the digest. - `certificate` (object, required) — The company certificate the seal was made with, kept for traceability. - `sealed_by_id` (string, required, format: uuid) — UUID v7 of the user who sealed the register. - `sealed_at` (string, required, format: date-time) — When the register was sealed (ISO 8601). - `verified` (boolean, required) — Live verification: `true` if the current snapshot and the signature are intact. - `verification_reason` (string, required, enum: `verified`, `snapshot_mismatch`, `signature_invalid`, `certificate_unreadable`) — Reason of the live verification: `verified` (intact), `snapshot_mismatch` (the snapshot changed since sealing), `signature_invalid` (the signature no longer validates) or `certificate_unreadable` (the certificate could not be read). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/monthly-time-record-closes/{monthly_time_record_close} — Retrieve a monthly time record close - **Operation ID**: `public-api.v1.monthly_time_record_closes.show` - **Tag**: Monthly Register Closes - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.show Retrieve a single monthly close by its `id` (UUID v7). A close belonging to another company returns 404 `monthly_time_record_close_not_found` (anti-enumeration). ## Path parameters - `monthly_time_record_close` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (MonthlyTimeRecordClose), required) — An immutable monthly close of the time record register for the Control Horario (time tracking) module. Closing a finished `(year, month)` freezes each active employee’s balance snapshot and locks the period against retroactive entries and corrections. The close can be reopened for an audited correction (state `closed ⇄ reopened`). - `id` (string, required, format: uuid) — Opaque identifier of the monthly close, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `monthly_time_record_close`) — Always `monthly_time_record_close`. - `year` (integer, required) — Year of the closed period. - `month` (integer, required, min 1, max 12) — Month of the closed period (1-12). - `status` (string, required, enum: `closed`, `reopened`) — State of the close: `closed` (immutable, period locked) or `reopened` (writes re-enabled for a correction). - `period_start` (string, required, format: date) — First day of the closed period (YYYY-MM-DD). - `period_end` (string, required, format: date) — Last day of the closed period (YYYY-MM-DD). - `closed_by_id` (string, required, format: uuid) — UUID v7 of the user who closed the period. - `closed_at` (string, required, format: date-time) — When the period was closed (ISO 8601). - `reopened_by_id` (string | null, required, format: uuid) — UUID v7 of the user who reopened the close; `null` while it is still `closed`. - `reopened_at` (string | null, required, format: date-time) — When the close was reopened (ISO 8601); `null` while it is still `closed`. - `employee_count` (integer, required) — Number of employees included in the close. - `is_sealed` (boolean, required) — Whether a digital seal exists for this close (mere presence; integrity verification is served by `GET /monthly-time-record-closes/{id}/seal`). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/monthly-time-record-closes/{monthly_time_record_close}/payroll-export — Download the payroll export of a closed month - **Operation ID**: `public-api.v1.monthly_time_record_closes.payroll_export` - **Tag**: Payroll Exports - **Required scope**: `payroll_exports:read` — Read payroll exports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/payroll-exports/public-api.v1.monthly_time_record_closes.payroll_export Download the payroll incidents file of a closed month in the format of a Spanish payroll software (`a3` for A3 Wolters Kluwer, `sage` for Sage, `nominasol` for NominaSOL), read from the frozen snapshot of the monthly close without recomputation. Each row is one employee with their fiscal identity (tax ID and name), worked vs expected minutes, overtime, balance and the approved absences broken down by type. `format` is optional and defaults to `a3`; a format outside the catalog returns 422. A period without a close returns 404. The response is a binary spreadsheet download. ## Path parameters - `monthly_time_record_close` (string, required) ## Query parameters - `format` (string | null, optional, enum: `a3`, `sage`, `nominasol`) — Payroll export format by software (defaults to a3). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - object - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/payroll-export-formats — List the supported payroll export formats - **Operation ID**: `public-api.v1.payroll_export_formats.list` - **Tag**: Payroll Exports - **Required scope**: `payroll_exports:read` — Read payroll exports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/payroll-exports/public-api.v1.payroll_export_formats.list List the payroll software formats supported by the payroll export (`a3`, `sage`, `nominasol`), each with its commercial label, so an integration can offer a software selector without hardcoding the values. A flat read-only catalog with no pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — Array of `PayrollExportFormatResource` - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `payroll_export_format`) — Always `payroll_export_format`. - `value` (string, required, enum: `a3`, `sage`, `nominasol`) — Stable identifier of the payroll software (`a3`, `sage` or `nominasol`). - `label` (string, required) — Human-readable commercial name of the payroll software (Spanish). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/presence/daily — List office/remote presence declarations - **Operation ID**: `public-api.v1.presence.daily` - **Tag**: Presence - **Required scope**: `presence:read` — Read presence. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/presence/public-api.v1.presence.daily List the office/remote presence declarations of your company with cursor-based pagination. Supports filtering by `employee_id` (UUID v7), by exact day (`date`) or by date range (`from`/`to`, `YYYY-MM-DD`). Each record is one employee’s declared work location for one day. Read-only over the public API — declarations are made from the app (SPA-only). ## Query parameters - `employee_id` (string | null, optional, format: uuid) — Employee ID (UUID v7) to filter presence by. - `date` (string | null, optional, format: date) — Exact day (Y-m-d) to filter presence by. - `from` (string | null, optional, format: date) — Start date (Y-m-d) to filter the presence range by. - `to` (string | null, optional, format: date) — End date (Y-m-d) to filter the presence range by. - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the presence record, a UUID v7. - `object` (string, required, enum: `daily_presence`) — Always `daily_presence`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the declaration belongs to. - `date` (string, required, format: date) — Day of the declaration (`YYYY-MM-DD`). - `location` (string, required, enum: `office`, `remote`) — Declared work location: `office` or `remote`. - `declared_at` (string | null, required, format: date-time) — Timestamp of the declaration (ISO 8601), or `null` when not retained. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/presence — Get the live team presence - **Operation ID**: `public-api.v1.presence.live` - **Tag**: Presence - **Required scope**: `presence:read` — Read presence. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/presence/public-api.v1.presence.live Return the live presence panel of your team for the Control Horario (time tracking) module: one `employee_presence` item per active employee, with the workday state derived from the immutable time record ledger (`working`/`paused`/`finished`/`away`), the late-arrival flag (first clock-in vs planned start) and the office/remote location declared today. A computed read-only resource: each item exposes the employee UUID v7 as its `id`, never a presence record id. No filters or pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — UUID v7 of the employee this presence belongs to (the resource has no id of its own). - `object` (string, required, enum: `employee_presence`) — Always `employee_presence`. - `display_name` (string, required) — Display name of the employee. - `work_state` (string, required, enum: `working`, `paused`, `finished`, `away`) — Derived workday state from the ledger: `working`, `paused`, `finished` (clocked out) or `away` (no open workday). - `since` (string | null, required, format: date-time) — Start of the current open segment (ISO 8601), or `null` when there is no open workday. - `first_clock_in_at` (string | null, required, format: date-time) — First clock-in of the day (ISO 8601), or `null` when the employee has not clocked in today. - `is_late` (boolean, required) — true when the employee clocked in after their planned start time. - `planned_start_at` (string | null, required, format: date-time) — Planned start time of the day (ISO 8601), or `null` on a rest day / when no schedule applies. - `location` (string | null, required, enum: `office`, `remote`, `null`) — Declared work location for today: `office`, `remote`, or `null` when not declared. - `location_declared` (boolean, required) — true when the employee has declared a work location today (distinguishes "office not declared" from "no data"). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/presence/{employee} — Retrieve an employee’s live presence - **Operation ID**: `public-api.v1.presence.show` - **Tag**: Presence - **Required scope**: `presence:read` — Read presence. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/presence/public-api.v1.presence.show Retrieve the live presence of a single employee by its `id` (UUID v7): the workday state derived from the ledger, the late-arrival flag and the office/remote location declared today. An employee that does not exist or belongs to another company returns 404 `employee_presence_not_found` (anti-enumeration). A computed resource: it exposes the employee UUID v7 as its `id`. ## Path parameters - `employee` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeePresence), required) — Live presence of an employee for the Control Horario (time tracking) module: a read-only projection derived from the immutable time record ledger and the employee’s planned schedule. `work_state` is the derived workday state; `is_late` is true when the first clock-in of the day happened after the planned start time. A computed resource with no entity identity of its own: its `id` is the UUID v7 of the employee, never a presence record id. - `id` (string, required, format: uuid) — UUID v7 of the employee this presence belongs to (the resource has no id of its own). - `object` (string, required, enum: `employee_presence`) — Always `employee_presence`. - `display_name` (string, required) — Display name of the employee. - `work_state` (string, required, enum: `working`, `paused`, `finished`, `away`) — Derived workday state from the ledger: `working`, `paused`, `finished` (clocked out) or `away` (no open workday). - `since` (string | null, required, format: date-time) — Start of the current open segment (ISO 8601), or `null` when there is no open workday. - `first_clock_in_at` (string | null, required, format: date-time) — First clock-in of the day (ISO 8601), or `null` when the employee has not clocked in today. - `is_late` (boolean, required) — true when the employee clocked in after their planned start time. - `planned_start_at` (string | null, required, format: date-time) — Planned start time of the day (ISO 8601), or `null` on a rest day / when no schedule applies. - `location` (string | null, required, enum: `office`, `remote`, `null`) — Declared work location for today: `office`, `remote`, or `null` when not declared. - `location_declared` (boolean, required) — true when the employee has declared a work location today (distinguishes "office not declared" from "no data"). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/{product}/activities — List product activity timeline - **Operation ID**: `public-api.v1.products.activities` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.activities Return the audit timeline for a product combining its own domain events plus document events whose lines reference it. Paginated with page and per_page query params (default 50). ## Path parameters - `product` (string, required) ## Query parameters - `per_page` (integer, optional, default: `50`) - `page` (integer, optional, default: `1`) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `ProductActivityV1Collection` - Body (`application/json`): - `data` (array, required) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `product.updated`, `invoice.created`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `total` (integer, required) — Total number of available activities. - `current_page` (integer, required) — Current page (1-based). - `per_page` (integer, required) — Number of activities per page. - `from` (integer | null, required) — Index of the first activity on the page, or `null` if empty. - `to` (integer | null, required) — Index of the last activity on the page, or `null` if empty. - `last_page` (integer, required) — Number of the last available page. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/bulk-delete — Delete multiple products in bulk - **Operation ID**: `public-api.v1.products.bulk_delete` - **Tag**: Products - **Required scope**: `products:delete` — Delete products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.bulk_delete Delete up to 200 products in one request. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`); products included in packs are reported in `failures`. Decrements the plan usage counter accordingly. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Public REST API v1 — POST /v1/products/bulk-delete. Body: `{ ids: string[] }`. Accepts between 1 and 200 IDs (UUID v7). Tenant membership validation is performed by the Handler (filtered by company_id); foreign IDs are silently ignored and will appear in `skipped`. - `ids` (array, required, maxItems 200) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/bulk-status — Bulk change product active state - **Operation ID**: `public-api.v1.products.bulk_status` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.bulk_status Move up to 50 products (by id) to the target `new_status` (`active` or `inactive`). Idempotent with respect to the target: a product already in the requested state counts as `successful` without flipping. Returns a `BulkPartialSuccessResult`; products not found come back in `failures[]`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `new_status`, `ids`. Transition several products to `new_status` (`active` or `inactive`) in one request, up to 50 per batch. `ids` is an array of product UUIDs; the change is idempotent (a product already in the target status counts as successful). Products that do not exist are returned under `failures[]`. - `new_status` (string, required, enum: `active`, `inactive`) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/bulk-update-stock — Update stock for many products - **Operation ID**: `public-api.v1.products.bulk_update_stock` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.bulk_update_stock Apply a stock operation to multiple products in one request (up to 500). UUIDs that do not belong to your company are ignored silently. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `updates`. Public REST API v1 — POST /v1/products/bulk-update-stock. Body: `{ updates: [{ product_id: string, stock: int, operation?: 'set'|'add'|'subtract' }] }`. Accepts up to 500 updates in a single operation. We accept `items` as an alias of the canonical `updates` field for forgiveness with integrators following the most common convention. The controller normalizes it to `updates`. - `updates` (array, required, maxItems 500) - `product_id` (string, required, format: uuid) - `stock` (integer, required, min 0) - `operation` (string | null, optional, enum: `set`, `increase`, `decrease`, `add`, `subtract`) ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `object` (string, required, const: `product_stock_bulk_update_result`) - `updated` (integer, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products — Create a product - **Operation ID**: `public-api.v1.products.create` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.create Create a new product in your catalog. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 13 properties; 2 required: `name`, `price`. Create a product in your catalog. Required: `name` and `price`. Optional: `sku` (unique per company), `description`, `tags`, `stock` (initial quantity), `low_stock_threshold`, `manage_stock`, `currency` (`EUR` only — any other code returns 422), `tax_rate_id`, `is_active`, `metadata` and `external_id`. Stock changes after creation are made through `PUT /v1/products/{product}/stock`, not through this endpoint. - `name` (string, required, maxLength 200) - `sku` (string | null, optional, maxLength 100) — Stock keeping unit (your own product code), unique per company. Optional; up to 100 characters. - `price` (string, required, pattern: `^-?\d+(\.\d{1,2})?$`) - `description` (string | null, optional) — Columna `products.description` es `text` → sin `max` artificial. - `stock` (integer | null, optional, min 0) — Initial stock (create only; later stock changes are made via `PUT /v1/products/{uuid}/stock`). Absent → defaults to 0. - `low_stock_threshold` (integer | null, optional, min 0) — Umbral per-producto; se persiste en `metadata.low_stock_threshold`. - `manage_stock` (boolean | null, optional) — Whether this product takes part in document-driven stock movements: with `true`, issuing or receiving a document that includes it moves its stock automatically and the movement is recorded in the stock ledger. It only takes effect if your company also has stock management enabled; absent or `null` means `false`. - `currency` (string | null, optional, enum: `EUR`) — Producto es read-only EUR: solo se admite `EUR` (o ausencia/null). Cualquier otra moneda → 422 (antes se aceptaba-y-descartaba). - `tax_rate_id` (string | null, optional, format: uuid) — `taxes` is a global system catalog (without a `company_id` column). Do NOT use TenantRule here — it would add `WHERE company_id = X` against a table without that column and cause a 500 (SQLSTATE 42S22). Global validation by uuid, like in the rest of the BCs (DeliveryNote V1, etc.). - `is_active` (boolean | null, optional) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — Third-party integration key from your ERP/CRM (orthogonal to `sku`). Free-form, up to 100 characters. - `tags` (array | null, optional) ## Responses - **201** — Product created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/products/{product} — Delete a product - **Operation ID**: `public-api.v1.products.delete` - **Tag**: Products - **Required scope**: `products:delete` — Delete products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.delete Delete a product. Returns 422 if the product is referenced by any document line. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/find-by-external-id — Find a product by external ID - **Operation ID**: `public-api.v1.products.find_by_external_id` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.find_by_external_id Look up a single product by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Orthogonal to the catalog `sku`. Returns the matching product or 404 if no product uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a product by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. The value travels in the body (not the URL) because an external key may contain characters that would break a path. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/find-by-sku — Find a product by SKU - **Operation ID**: `public-api.v1.products.find_by_sku` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.find_by_sku Look up a single product by its `sku` (sent in the JSON body). Returns the matching product or 404 if no product uses that SKU within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `sku`. Public REST API v1 — POST /v1/products/find-by-sku. Looks up a product by its `sku` within the authenticated company. The `sku` travels in the body (not in the URL) so as not to expose SKUs in logs/URLs and because a SKU may contain valid `.` and `/` characters that would break the path. - `sku` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/products/{product}/gallery/{index} — Remove a gallery image from a product - **Operation ID**: `public-api.v1.products.gallery.delete` - **Tag**: Products - **Required scope**: `products:delete` — Delete products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.gallery.delete Delete a gallery image by its 0-based index. Remaining images shift positions to fill the gap. ## Path parameters - `product` (string, required) - `index` (integer, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/{product}/gallery/{index}/download — Download a product gallery image binary - **Operation ID**: `public-api.v1.products.gallery.download` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.gallery.download Stream the raw binary of a product gallery image by its 0-based index. Returns 404 if the index is missing or the file is not on disk. ## Path parameters - `product` (string, required) - `index` (integer, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/octet-stream`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/{product}/gallery — Upload a gallery image to a product - **Operation ID**: `public-api.v1.products.gallery.upload` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.gallery.upload Attach an image (jpeg, png, jpg, gif or webp; up to 3 MB) to the product gallery. Returns the updated product. Fails with 422 if the gallery limit is exceeded. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `multipart/form-data`, required. 1 property; 1 required: `image`. Public REST API v1 — POST /v1/products/{uuid}/gallery. Multipart upload: `photo` or `image` (alias) field — jpeg/png/jpg/gif/webp, max 3 MB. We accept `image` as an alias of the canonical `photo` field for forgiveness with integrators that send it following the more intuitive convention. The controller normalizes it to `photo`. - `image` (string, required, format: binary, maxLength 3072) ## Responses - **201** - Body (`application/json`): - `data` (object, required) - `object` (string, required, const: `product_image`) - `index` (integer, required) — Position (0-based) of the newly uploaded image in the gallery. - `url` (string, required, format: uri) — Public URL of the uploaded image. - `content_type` (string, required, enum: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `application/octet-stream`) — MIME type de la imagen. - `gallery_total` (integer, required) — Total number of images in the gallery after the upload. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products — List all products - **Operation ID**: `public-api.v1.products.list` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.list List products in your catalog with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `sku` (string, optional) — Product SKU. - `sku[in]` (string, optional) — Product SKU. - `sku[contains]` (string, optional) — Product SKU. - `name` (string, optional) — Product name. - `name[contains]` (string, optional) — Product name. - `is_active` (boolean, optional) — Filter by active / inactive products. - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `price[gte]` (number, optional) — Unit price. - `price[lte]` (number, optional) — Unit price. - `price[gt]` (number, optional) — Unit price. - `price[lt]` (number, optional) — Unit price. - `in_stock` (boolean, optional) — Filter by products with available stock (`true` → stock > 0) or out of stock (`false` → stock = 0). - `low_stock` (boolean, optional) — Filter by products with low stock (`true` → 0 < stock ≤ configured threshold) or with ample stock (`false` → stock > threshold). - `tag` (string, optional) — Filter by classification tag. - `tag[in]` (string, optional) — Filter by classification tag. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/low-stock-report — List products below the stock threshold - **Operation ID**: `public-api.v1.products.low_stock_report` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.low_stock_report Return products whose current stock is below their configured low-stock threshold. Useful for inventory alerts. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `total_count` (string, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/{product}/sales-analytics — Get product sales analytics - **Operation ID**: `public-api.v1.products.sales_analytics` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.sales_analytics Return units sold, revenue, invoice count, month-over-month delta, monthly trend for the last 6 months, last buyer and recent activity feed for a single product. ## Path parameters - `product` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/search — Search products - **Operation ID**: `public-api.v1.products.search` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.search Search products by free-text query against `name` and `sku`. Capped at 50 results. ## Query parameters - `q` (string, required, maxLength 120, minLength 1) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/{product} — Retrieve a product - **Operation ID**: `public-api.v1.products.show` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.show Retrieve a product by its `uuid`. ## Path parameters - `product` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/stats — Get product stats - **Operation ID**: `public-api.v1.products.stats` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.stats Aggregated KPIs for your product catalog: total product count, active count, count below the low-stock threshold, accumulated stock value, and totals by category. Returned as `{ "data": ProductStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ProductStats), required) — Aggregated summary of the product catalog of the authenticated company: total, active, out of stock and low stock. Returned by `GET /v1/products/stats`. - `total_products` (integer, required) — Total number of products registered in the company. - `active_products` (integer, required) — Productos marcados como activos. - `out_of_stock_count` (integer, required) — Productos sin stock disponible (stock = 0). - `low_stock_count` (integer, required) — Products whose stock is below the configured threshold. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/{product}/toggle-active — Toggle product active state - **Operation ID**: `public-api.v1.products.toggle_active` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.toggle_active Flip a product between active and inactive. Inactive products are hidden from line-item selectors on new documents. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/products/{product} — Update a product - **Operation ID**: `public-api.v1.products.update` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.update Update a product in your catalog. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 12 properties; none of them required. Public REST API v1 — PUT /v1/products/{uuid}. Full update (PUT). All fields `sometimes`: if not sent, the handler keeps the current value. `sku` unique scoped to the company, ignoring the product itself. Writable: `name`, `sku`, `price`, `description`, `tags`, `low_stock_threshold` (per-product), `manage_stock` (document-driven stock movements flag, PATCH-style), `currency` (EUR only — Producto is read-only EUR; any other code → 422), `tax_rate_id`, `is_active`, `metadata`, `external_id`. `stock` is NOT writable here (D1): stock mutation lives only in `PUT /v1/products/{uuid}/stock` with its `set`/`increase`/`decrease` semantics. Validation of `metadata` via VO `Metadata`. - `name` (string, optional, maxLength 200) - `sku` (string | null, optional, maxLength 100) - `price` (string, optional, pattern: `^-?\d+(\.\d{1,2})?$`) - `description` (string | null, optional) — Columna `products.description` es `text` → sin `max` artificial. - `low_stock_threshold` (integer | null, optional, min 0) — Umbral per-producto; se persiste en `metadata.low_stock_threshold`. - `manage_stock` (boolean, optional) — Whether this product takes part in document-driven stock movements: with `true`, issuing or receiving a document that includes it moves its stock automatically and the movement is recorded in the stock ledger. It only takes effect if your company also has stock management enabled; absent or `null` means `false`. - `currency` (string | null, optional, enum: `EUR`) — Product amounts are read-only EUR: only `EUR` (or absence/null) is accepted. Stock is not writable here; stock changes are made via `PUT /v1/products/{uuid}/stock`. - `tax_rate_id` (string | null, optional, format: uuid) — `taxes` is a global system catalog (without a `company_id` column). Do NOT use TenantRule here — it would add `WHERE company_id = X` against a table without that column and cause a 500 (SQLSTATE 42S22). Global validation by uuid, like in the rest of the BCs (DeliveryNote V1, etc.). - `is_active` (boolean, optional) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — Third-party integration key from your ERP/CRM. Partial update: an absent value is preserved. Free-form, up to 100 characters; unique per company. - `tags` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/products/{product}/stock — Update product stock - **Operation ID**: `public-api.v1.products.update_stock` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.update_stock Replace, increase or decrease the stock quantity of a product. Defaults to set (replace); add and subtract are accepted aliases for increase and decrease. Fails with 422 if the resulting stock would be negative. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `stock`. Public REST API v1 — PUT /v1/products/{uuid}/stock. Body: `{ stock: int, operation?: 'set'|'increase'|'decrease' }`. `operation` defaults to `set` (replace). Accepts also `add` / `subtract` as aliases for `increase` / `decrease` for ergonomics. - `stock` (integer, required, min 0) - `operation` (string | null, optional, enum: `set`, `increase`, `decrease`, `add`, `subtract`) ## Responses - **200** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/products/{product}/video — Remove the product video - **Operation ID**: `public-api.v1.products.video.delete` - **Tag**: Products - **Required scope**: `products:delete` — Delete products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.video.delete Delete the video associated with the product and release the storage. Idempotent: returns 204 even when no video was attached. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/products/{product}/video/download — Download the product video binary - **Operation ID**: `public-api.v1.products.video.download` - **Tag**: Products - **Required scope**: `products:read` — Read products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.video.download Stream the raw binary of the product video. Returns 404 if the product has no video or the file is not on disk. ## Path parameters - `product` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/octet-stream`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/products/{product}/video — Upload a video to a product - **Operation ID**: `public-api.v1.products.video.upload` - **Tag**: Products - **Required scope**: `products:write` — Create and update products. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/products/public-api.v1.products.video.upload Attach a video file (mp4, mov, avi or webm; up to 50 MB) to the product. Replaces any existing video. ## Path parameters - `product` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `multipart/form-data`, required. 1 property; 1 required: `video`. Public REST API v1 — POST /v1/products/{uuid}/video. Multipart upload: campo `video` (mp4/mov/avi/webm, max 50 MB). - `video` (string, required, format: binary, maxLength 51200) ## Responses - **201** - Body (`application/json`): - `data` (object (Product), required) — A product in your catalog. - `id` (string, required) - `object` (string, required, enum: `product`) - `name` (string, required) - `sku` (string | null, required) - `price` (string, required, pattern: `^\d+\.\d{2}$`) — Monetary amount as a string with two decimal places (Stripe-style), e.g. "1234.56". - `currency` (string, required) - `tax_rate` (object (TaxRateRef) | null, required) - `stock` (integer, required) — Cantidad disponible en stock. - `manage_stock` (boolean, required, default: `false`) — Whether the product participates in document-driven stock movements (invoices, delivery notes). Requires the company `stock_management` module to be enabled. Defaults to `false`. - `gallery` (array, required) — Product gallery images. - `video` (object | null, required) — Product attached video, or `null` if there is none. - `is_active` (boolean, required) - `description` (string | null, required) — Free-text description of the product. - `tags` (array, required) — Product classification tags. - `low_stock_threshold` (integer | null, required) — Threshold below which stock is considered low, or `null` if not configured. - `is_low_stock` (boolean, required) — Indicates whether the current stock is below the configured threshold. - `is_in_stock` (boolean, required) — Indica si hay stock disponible (> 0). - `specifications` (object, required) — Structured technical specifications of the product (free key-value pairs). Empty object `{}` when there are no specifications. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this product to a record in a third-party system. Free-format, unique per company, orthogonal to the catalog `sku` (a product may have both, neither, or either). - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/{proforma}/accept — Accept a proforma - **Operation ID**: `public-api.v1.proformas.accept` - **Tag**: Proformas - **Required scope**: `proformas:transition` — Change the lifecycle status of proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.accept Mark a proforma as accepted by the client. Returns 422 if the proforma is in a status that cannot transition to `accepted`. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 2 properties; none of them required. Optional body to record the client acceptance: `reason` (string, ≤500 chars) and `metadata` (object, ≤50 keys, ≤500 chars per value). - `reason` (string | null, optional, maxLength 500) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/bulk-delete — Bulk delete proformas - **Operation ID**: `public-api.v1.proformas.bulk_delete` - **Tag**: Proformas - **Required scope**: `proformas:delete` — Delete proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.bulk_delete Deletes up to 100 proformas in one call. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each entry that could not be deleted. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Public REST API v1 — DELETE /v1/proformas/bulk. - `ids` (array, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/bulk-pdf — Bulk download proforma PDFs - **Operation ID**: `public-api.v1.proformas.bulk_pdf` - **Tag**: Proformas - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.bulk_pdf Packages the PDFs of up to 50 proformas (by id) into a single ZIP. Ids that are not found or have no generable PDF do not abort the request: the ZIP carries only the valid ones and the per-resource counts travel in the `X-Bulk-*` response headers. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Package the PDFs of several proformas into a single ZIP. `ids` is an array of proforma UUIDs, up to 50 per request. - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/zip`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/bulk-send — Bulk send proformas - **Operation ID**: `public-api.v1.proformas.bulk_send` - **Tag**: Proformas - **Required scope**: `proformas:send` — Send by email proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.bulk_send Sends up to 200 proformas by email (queued) in one call, reusing the single-send path per id. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each proforma that could not be sent (not found, non-sendable status or no resolvable recipient). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 1 required: `ids`. Email several proformas in one request (queued), up to 200 per batch. `ids` is an array of proforma UUIDs; the optional `to`/`cc` arrays and `subject`/`message`/`language` overrides apply to the whole batch (when `to` is omitted, each proforma uses its client email). - `subject` (string | null, optional, maxLength 200) - `message` (string | null, optional, maxLength 5000) - `language` (string | null, optional, maxLength 5) - `ids` (array, required, maxItems 200) - `to` (array | null, optional) - `cc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/bulk-status — Bulk change proforma status - **Operation ID**: `public-api.v1.proformas.bulk_status` - **Tag**: Proformas - **Required scope**: `proformas:transition` — Change the lifecycle status of proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.bulk_status Transition up to 50 proformas (by id) to a status from the closed set `[accepted, rejected]`, each through the document state guard. Returns a `BulkPartialSuccessResult`; proformas whose transition is rejected (not found or not transitionable) come back in `failures[]`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `new_status`, `ids`. Transition several proformas to `new_status` (`accepted` or `rejected`) in one request, up to 50 per batch. `ids` is an array of proforma UUIDs; every transition passes the document state guard, and proformas that cannot transition are returned under `failures[]`. - `new_status` (string, required, enum: `accepted`, `rejected`) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/{proforma}/convert — Convert proforma to invoice - **Operation ID**: `public-api.v1.proformas.convert` - **Tag**: Proformas - **Required scope**: `proformas:transition` — Change the lifecycle status of proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.convert Convert a proforma into a final sales invoice. The new invoice references the source proforma; the proforma moves to status `converted`. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `target`. Public REST API v1 — POST /v1/proformas/{uuid}/convert. Required body: `target` ∈ {invoice}. Only conversion to invoice is supported — other targets (`proforma`, `delivery_note`) do NOT apply because a proforma can only be converted to an invoice by BC design. - `target` (string, required, enum: `invoice`) ## Responses - **201** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas — Create a proforma - **Operation ID**: `public-api.v1.proformas.create` - **Tag**: Proformas - **Required scope**: `proformas:write` — Create and update proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.create Create a new proforma invoice in `draft` status. Proformas can later be converted to final invoices. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 19 properties; 3 required: `client_id`, `issued_on`, `lines`. - `client_id` (string, required, format: uuid) - `series_id` (string | null, optional, format: uuid) - `issued_on` (string, required, format: date) - `valid_until` (string | null, optional, format: date) - `validity_days` (integer | null, optional, min 1, max 365) - `notes` (string | null, optional, maxLength 1000) - `terms_and_conditions` (string | null, optional, maxLength 2000) - `reference` (string | null, optional, maxLength 255) - `payment_method` (string | null, optional, maxLength 100) - `payment_terms` (integer | null, optional, min 0, max 365) - `shipping_cost` (number | null, optional, min 0) - `delivery_terms` (string | null, optional, maxLength 255) - `estimated_delivery_date` (string | null, optional, format: date) - `external_id` (string | null, optional, maxLength 100) - `currency` (string | null, optional, enum: `EUR`) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, required) - `description` (string, required, maxLength 255) - `quantity` (number, required, min 0.01) - `unit_price` (number, required, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **201** — Proforma created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/proformas/{proforma} — Delete a proforma - **Operation ID**: `public-api.v1.proformas.delete` - **Tag**: Proformas - **Required scope**: `proformas:delete` — Delete proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.delete Delete a proforma. Returns 422 if the proforma has been converted to an invoice. ## Path parameters - `proforma` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/{proforma}/duplicate — Duplicate a proforma - **Operation ID**: `public-api.v1.proformas.duplicate` - **Tag**: Proformas - **Required scope**: `proformas:write` — Create and update proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.duplicate Create a new draft proforma by copying lines, client, and metadata from an existing proforma. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/find-by-external-id — Find a proforma by external ID - **Operation ID**: `public-api.v1.proformas.find_by_external_id` - **Tag**: Proformas - **Required scope**: `proformas:read` — Read proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.find_by_external_id Look up a single proforma by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Returns the matching proforma or 404 `proforma_not_found` if no proforma uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a proforma by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/proformas — List all proformas - **Operation ID**: `public-api.v1.proformas.list` - **Tag**: Proformas - **Required scope**: `proformas:read` — Read proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.list List your proforma invoices with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Proforma status. - `status[in]` (string, optional) — Proforma status. - `client_id` (string, optional, format: uuid) — Client ID (UUID v7). - `client_id[in]` (string, optional) — Client ID (UUID v7). - `series_id` (string, optional, format: uuid) — Series ID (UUID v7). - `series_id[in]` (string, optional) — Series ID (UUID v7). - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `total[gte]` (number, optional) — Total amount. - `total[lte]` (number, optional) — Total amount. - `total[gt]` (number, optional) — Total amount. - `total[lt]` (number, optional) — Total amount. - `number` (string, optional) — Proforma number. - `number[contains]` (string, optional) — Proforma number. - `tags` (string, optional) — Filter by classification tag (lowercase slug). - `tags[in]` (string, optional) — Filter by classification tag (lowercase slug). - `sort` (string, optional, enum: `created`, `-created`, `total`, `-total`, `number`, `-number`, `valid_until`, `-valid_until`) — Sort order. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/proformas/{proforma}/pdf — Download proforma PDF - **Operation ID**: `public-api.v1.proformas.pdf` - **Tag**: Proformas - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.pdf Download the PDF representation of a proforma. ## Path parameters - `proforma` (string, required) ## Query parameters - `download` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - string - **304** - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/proformas/{proforma}/public-link — Retrieve proforma public link - **Operation ID**: `public-api.v1.proformas.public_link_get` - **Tag**: Proformas - **Required scope**: `proformas:read` — Read proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.public_link_get Returns the shareable public URL of the proforma (/d/{uuid}) along with its status, expiration, and the plan-allowed maximum extension days. ## Path parameters - `proforma` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/proformas/{proforma}/public-link — Update proforma public link - **Operation ID**: `public-api.v1.proformas.public_link_update` - **Tag**: Proformas - **Required scope**: `proformas:write` — Create and update proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.public_link_update Applies an action to the public link: `revoke`, `activate`, `extend` (with `extend_days`), or `reset` to the plan default. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `action`. Public REST API v1 — PUT /v1/proformas/{uuid}/public-link. `SchemaName` disambiguates the OpenAPI schema: Quote and Proforma declare structurally identical request bodies, so without a unique name they would collide in `components.schemas`. - `action` (string, required, enum: `revoke`, `activate`, `extend`, `reset`) - `extend_days` (integer, optional, min 1, max 36500) ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/{proforma}/reject — Reject a proforma - **Operation ID**: `public-api.v1.proformas.reject` - **Tag**: Proformas - **Required scope**: `proformas:transition` — Change the lifecycle status of proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.reject Mark a proforma as rejected by the client. Returns 422 if the proforma is in a status that cannot transition to `rejected`. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 2 properties; none of them required. Optional body to record the client rejection: `reason` (string, ≤500 chars) and `metadata` (object, ≤50 keys, ≤500 chars per value). - `reason` (string | null, optional, maxLength 500) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/proformas/{proforma}/send — Send proforma by email - **Operation ID**: `public-api.v1.proformas.send` - **Tag**: Proformas - **Required scope**: `proformas:send` — Send by email proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.send Send a proforma to the client by email. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 5 properties; none of them required. Public REST API v1 — POST /v1/proformas/{uuid}/send. Optional body: `to` (string), `cc[]`, `bcc[]` (arrays of emails), `subject` (max 200), `body` (string). The controller performs the cross-field validation: if the client has no email and `to` is absent, it returns 422 `missing_required_param`. - `to` (string | null, optional, format: email, maxLength 191) - `subject` (string | null, optional, maxLength 200) - `body` (string | null, optional, maxLength 5000) - `cc` (array | null, optional) - `bcc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/proformas/{proforma} — Retrieve a proforma - **Operation ID**: `public-api.v1.proformas.show` - **Tag**: Proformas - **Required scope**: `proformas:read` — Read proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.show Retrieve a proforma invoice by its `uuid`. ## Path parameters - `proforma` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/proformas/stats — Get proforma stats - **Operation ID**: `public-api.v1.proformas.stats` - **Tag**: Proformas - **Required scope**: `proformas:read` — Read proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.stats Aggregated KPIs for the authenticated company: total proforma count and amount, count per status, expired count, and count converted to invoice. Returned as `{ "data": ProformaStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ProformaStats), required) — Aggregated summary of the proformas of the authenticated company: total, accumulated amount, breakdown by status (with count and amount), conversion-to-invoice ratio, those about to expire and average value. Returned by `GET /v1/proformas/stats`. - `total_count` (integer, required) — Total number of recorded proformas. - `total_amount` (number, required) — Aggregate amount of the proformas (EUR). - `by_status` (object, required) — Breakdown by status. Keys: `draft`, `accepted`, `rejected`, `cancelled`, `expired`, `converted`. - `conversion_rate` (number, required) — Conversion-to-invoice ratio (converted proformas / total), as a fraction. - `expiring_soon` (integer, required) — Proformas whose `valid_until` date expires in the coming days. - `average_value` (number, required) — Average value of the proformas (EUR). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/proformas/statuses — List proforma statuses - **Operation ID**: `public-api.v1.proformas.statuses` - **Tag**: Proformas - **Required scope**: `proformas:read` — Read proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.statuses Returns the closed catalog of proforma statuses with their public `value`, localized `label`, and UI `color`. Use it to populate filters or status pickers instead of hard-coding values. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/proformas/{proforma} — Update a proforma - **Operation ID**: `public-api.v1.proformas.update` - **Tag**: Proformas - **Required scope**: `proformas:write` — Create and update proformas. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/proformas/public-api.v1.proformas.update Update a draft proforma. Once converted, the proforma becomes immutable. ## Path parameters - `proforma` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 18 properties; none of them required. Public REST API v1 — PUT /v1/proformas/{uuid}. Partial update: omitted fields are kept. Only allowed when the proforma is in `draft` status (the controller maps the transition exception to 422 `invalid_status_transition`). - `client_id` (string, optional, format: uuid) - `issued_on` (string, optional, format: date) - `valid_until` (string | null, optional, format: date) - `validity_days` (integer | null, optional, min 1, max 365) - `notes` (string | null, optional, maxLength 1000) - `terms_and_conditions` (string | null, optional, maxLength 2000) - `reference` (string | null, optional, maxLength 255) - `payment_method` (string | null, optional, maxLength 100) - `payment_terms` (integer | null, optional, min 0, max 365) - `shipping_cost` (number | null, optional, min 0) - `delivery_terms` (string | null, optional, maxLength 255) - `estimated_delivery_date` (string | null, optional, format: date) - `operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`) - `external_id` (string | null, optional, maxLength 100) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional, min 0.01) - `unit_price` (number, optional, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **200** - Body (`application/json`): - `data` (object (Proforma), required) — A proforma invoice that can be converted to a final invoice. - `id` (string, required) - `object` (string, required, enum: `proforma`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Proforma lifecycle status (draft, accepted, rejected, cancelled, expired, converted). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `validity_days` (integer | null, required) — Number of validity days of the proforma since its issuance. `null` if not applicable. - `reference` (string | null, required) — Free reference of the document (e.g. the customer order number). - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this proforma was converted into, if applicable. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this proforma was converted into (e.g. "F-2026-00042"). `null` if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregate tax amount (= total_vat + total_surcharge − total_retention). Use total_vat/total_retention/total_surcharge for the breakdown. - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total` (number, required) - `shipping_cost` (number, required) — Additional shipping cost added to the total. - `total_with_shipping` (number, required) — Final total including the shipping cost (= total + shipping_cost). - `currency` (string, required) - `payment_method` (string | null, required) — Preferred payment method (backing value of the Shared enum `PaymentMethod`, e.g. `bank_transfer`). - `payment_terms_days` (integer | null, required, min 0, max 365) — Payment term in days (Net X). Valid range: 0-365. - `delivery_terms` (string | null, required) — Condiciones de entrega en formato libre. - `estimated_delivery_date` (string | null, required, format: date) — Estimated delivery date (YYYY-MM-DD). - `notes` (string | null, required) - `terms_and_conditions` (string | null, required) — Terms and conditions rendered in the proforma PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiration date of the shareable public link, or `null` if unlimited. - `link_is_active` (boolean, required) — Indicates whether the shareable public link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The proforma request conflicts with its current state — e.g. an invalid status transition (re-converting an already-converted proforma), an attempt to edit a sent proforma, or a reused idempotency key. - **422** — Validation failed, or the proforma cannot undergo the requested state transition (e.g. editing an already-sent proforma). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices/{purchase_invoice}/attach-file — Attach a file to a purchase invoice - **Operation ID**: `public-api.v1.purchase_invoices.attach_file` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:write` — Create and update purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.attach_file Upload the original PDF document for a purchase invoice as `multipart/form-data`. Replaces any previously attached file. Returns the updated purchase invoice. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `multipart/form-data`, required. 1 property; 1 required: `file`. - `file` (string, required, format: binary, maxLength 51200) ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices/bulk-delete — Bulk delete purchase invoices - **Operation ID**: `public-api.v1.purchase_invoices.bulk_delete` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:delete` — Delete purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.bulk_delete Delete up to 100 purchase invoices by UUID in a single request. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each entry that could not be deleted. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Delete several purchase invoices in one request. `ids` is an array of 1 to 100 UUIDs; unknown identifiers are reported as failed rather than failing the whole request. - `ids` (array, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices/bulk-status — Bulk change purchase invoice status - **Operation ID**: `public-api.v1.purchase_invoices.bulk_status` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:transition` — Change the lifecycle status of purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.bulk_status Transition up to 50 purchase invoices (by id) to `paid` in one call, each through the document state guard. The required `payment_date` is propagated as-is to every invoice (never `now()`). Returns a `BulkPartialSuccessResult`; invoices that could not transition (not found or already paid) come back in `failures[]`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `new_status`, `ids`. Transition several purchase invoices to `new_status` (`paid`) in one request, up to 50 per batch. `ids` is an array of purchase-invoice UUIDs; `payment_date` is required and cannot be in the future. Every transition passes the document state guard, and invoices that cannot transition are returned under `failures[]`. - `new_status` (string, required, enum: `paid`) - `payment_date` (string | null, optional, format: date-time) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices — Create a purchase invoice - **Operation ID**: `public-api.v1.purchase_invoices.create` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:write` — Create and update purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.create Record an invoice received from a supplier. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 25 properties; 3 required: `external_invoice_number`, `issued_on`, `lines`. - `is_simplified` (boolean, optional) — Factura simplificada (ticket de gasto): con `is_simplified: true` el proveedor pasa a opcional. El número del proveedor (`external_invoice_number`) SIGUE siendo obligatorio en v1/MCP (el recurso se recupera por número tras crear). - `supplier_id` (string | null, optional, format: uuid) - `expense_category_id` (string | null, optional, format: uuid) - `external_invoice_number` (string, required, maxLength 255) - `external_id` (string | null, optional, maxLength 100) — External business key (integration key from your ERP/CRM), unique per company. Orthogonal to `external_invoice_number` (the supplier fiscal number): `external_id` is the resource ID in the integrator system. - `internal_code` (string | null, optional, maxLength 255) - `issued_on` (string, required, format: date) - `received_on` (string | null, optional, format: date) - `due_on` (string | null, optional, format: date) - `status` (string, optional, enum: `draft`, `pending`) — Optional initial status. 4-state model: CREATION allowlist `draft|pending` (`received`/`pending_payment` were merged into `pending`). `paid|cancelled` are lifecycle transitions (mark_paid/change_status), NOT creation states. If omitted, the domain applies the default `draft`. - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `internal_notes` (string | null, optional, maxLength 2000) — Extend fields. Basic SHAPE only; the `payment_method` allowlist, `tax_period` format and `tags` cardinality are validated by the VO/Aggregate. - `payment_method` (string | null, optional, maxLength 30) - `payment_terms_days` (integer | null, optional, min 0, max 365) - `bank_account_id` (integer | null, optional) - `expense_account` (string | null, optional, maxLength 50) - `tax_period` (string | null, optional, maxLength 10) - `is_reverse_charge` (boolean | null, optional) - `deductible_percentage` (number | null, optional, min 0, max 100) - `operation_class` (string | null, optional, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Classifies the origin of the expense for the input VAT of Modelo 303 (boxes [28]-[39]). Closed set; nullable → defaults to `corriente`. - `exclude_347` (boolean, optional) — Whether to exclude this purchase invoice from the annual Modelo 347 declaration. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) — Typed custom fields as `[{field, value}]`. `field` up to 60 characters (non-empty), `value` up to 500 characters, up to 50 entries. - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, required) - `description` (string, required, maxLength 255) - `quantity` (number, required, min 0.01) - `unit_price` (number, required, min 0) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) — Per-line IRPF retention and deductible VAT. `retention_rate` is the percentage withheld from the supplier (0–100); `vat_deductible` flags VAT deductibility (informational, does not change the amount paid). - `vat_deductible` (boolean | null, optional) - `surcharge_rate` (number | null, optional, min 0, max 100) — Per-line equivalence surcharge (the legal VAT↔surcharge pair is validated) and LIVA exemption reason (closed catalog). - `exemption_reason` (string | null, optional, enum: `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) — Per-line indirect tax regime of the supplier. Passthrough only (purchase lines have no `tax_id`): the incoming override is the sole regime channel; the real invariant lives in the domain. ## Responses - **201** — Purchase invoice created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/purchase_invoices/{purchase_invoice} — Delete a purchase invoice - **Operation ID**: `public-api.v1.purchase_invoices.delete` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:delete` — Delete purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.delete Delete a purchase invoice. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/purchase_invoices/{purchase_invoice}/file — Remove a purchase invoice file - **Operation ID**: `public-api.v1.purchase_invoices.delete_file` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:write` — Create and update purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.delete_file Delete the original file attached to a purchase invoice and release its storage. Idempotent: succeeds even when no file was attached. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/{purchase_invoice}/file — Download the original purchase invoice file - **Operation ID**: `public-api.v1.purchase_invoices.file` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.file Stream the original PDF attached to the purchase invoice when it was uploaded. Returns 404 if no attachment is present. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — El adjunto está cifrado at-rest en el Vault; el handler entrega un temp file DESCIFRADO de vida acotada que se elimina tras enviarse. - Body (`application/json`): - object - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices/find-by-external-id — Find a purchase invoice by external ID - **Operation ID**: `public-api.v1.purchase_invoices.find_by_external_id` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.find_by_external_id Look up a single purchase invoice by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Orthogonal to the supplier-provided `external_invoice_number` (the vendor's fiscal number). Returns the matching purchase invoice or 404 `purchase_invoice_not_found` if none uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a purchase invoice by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. Orthogonal to `external_invoice_number`, the supplier fiscal number. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices — List all purchase invoices - **Operation ID**: `public-api.v1.purchase_invoices.list` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.list List purchase invoices received from suppliers with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Purchase invoice status. - `status[in]` (string, optional) — Purchase invoice status. - `supplier_id` (string, optional, format: uuid) — Supplier ID (UUID v7). - `supplier_id[in]` (string, optional) — Supplier ID (UUID v7). - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `issued_on[gte]` (string, optional, format: date) — Issue date. - `issued_on[lte]` (string, optional, format: date) — Issue date. - `issued_on[gt]` (string, optional, format: date) — Issue date. - `issued_on[lt]` (string, optional, format: date) — Issue date. - `total[gte]` (number, optional) — Total amount. - `total[lte]` (number, optional) — Total amount. - `total[gt]` (number, optional) — Total amount. - `total[lt]` (number, optional) — Total amount. - `currency` (string, optional) — ISO 4217 currency code. - `currency[in]` (string, optional) — ISO 4217 currency code. - `external_invoice_number` (string, optional) — Invoice number issued by the supplier. - `external_invoice_number[contains]` (string, optional) — Invoice number issued by the supplier. - `tags` (string, optional) — Filter by classification tag (lowercase slug). - `tags[in]` (string, optional) — Filter by classification tag (lowercase slug). - `sort` (string, optional, enum: `created`, `-created`, `total`, `-total`, `issued_on`, `-issued_on`, `due_on`, `-due_on`) — Sort order. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/{purchase_invoice}/payments — List purchase invoice payments - **Operation ID**: `public-api.v1.purchase_invoices.list_payments` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.list_payments Return the full payment ledger of a purchase invoice as `{ "data": [...] }`, ordered by payment date descending. The ledger of a single invoice is bounded, so the complete set is returned without cursor pagination. An invoice with no payments returns an empty array, never a `404`; a `404` here means the invoice does not exist or belongs to another company. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices/{purchase_invoice}/mark_paid — Mark purchase invoice as paid - **Operation ID**: `public-api.v1.purchase_invoices.mark_paid` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:transition` — Change the lifecycle status of purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.mark_paid Record payment of a purchase invoice. Sets `paid_at` to the current timestamp. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 3 properties; none of them required. - `paid_on` (string | null, optional, format: date) - `payment_method` (string | null, optional, enum: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`) - `notes` (string | null, optional, maxLength 1000) ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/overdue — List overdue purchase invoices - **Operation ID**: `public-api.v1.purchase_invoices.overdue` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.overdue Return purchase invoices whose due date has passed and are still unpaid. ## Query parameters - `per_page` (string, optional, default: `"25"`) - `limit` (string, optional) — Default `'25'` (string) por consistencia OpenAPI: Scramble infiere schema.type=string para `request->input()` y el default debe ser string (Spectral rechaza `default: 25` int con `type: string`). - `cursor` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/{purchase_invoice}/payment-receipt — Download a purchase invoice payment receipt - **Operation ID**: `public-api.v1.purchase_invoices.payment_receipt` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.payment_receipt Stream the PDF payment receipt for a paid purchase invoice. Returns 409 if the invoice has not been paid yet. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/pending — List pending purchase invoices - **Operation ID**: `public-api.v1.purchase_invoices.pending` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.pending Return purchase invoices in pending payment status, paginated. ## Query parameters - `per_page` (string, optional, default: `"25"`) - `limit` (string, optional) — Default `'25'` (string) por consistencia OpenAPI/Spectral. - `cursor` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/purchase_invoices/{purchase_invoice}/payments — Register a purchase invoice payment - **Operation ID**: `public-api.v1.purchase_invoices.register_payment` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:transition` — Change the lifecycle status of purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.register_payment Record a partial (or total) payment against a purchase invoice and append it to its ledger. Body: `amount`, `paid_on`, `payment_method`, plus the optional `bank_account_id`, `reference` and `notes`. Three invariants are enforced and return `422`: the amount must be greater than zero and no larger than the outstanding balance, `paid_on` must fall between the invoice issue date and today, and a cancelled invoice accepts no payments. Once the accumulated payments cover the total, the invoice settles on its own — you do not need to call `mark_paid` as well. Returns `201` with the payment just created and a `Location` header pointing at the ledger. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 3 required: `amount`, `paid_on`, `payment_method`. Register a partial (or full) payment against a purchase invoice. Required: `amount` (> 0), `paid_on` (date) and `payment_method` (a value from the closed catalog). Optional: `bank_account_id`, `reference`, `notes`. The domain invariants (amount within the pending balance, issue date ≤ payment date ≤ today, invoice not cancelled) are enforced with a 422. - `amount` (number, required) - `paid_on` (string, required, format: date) - `payment_method` (string, required, maxLength 30) - `bank_account_id` (integer | null, optional) - `reference` (string | null, optional, maxLength 255) - `notes` (string | null, optional, maxLength 1000) ## Responses - **201** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/{purchase_invoice} — Retrieve a purchase invoice - **Operation ID**: `public-api.v1.purchase_invoices.show` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.show Retrieve a purchase invoice by its `uuid`. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/purchase_invoices/stats — Get purchase invoice stats - **Operation ID**: `public-api.v1.purchase_invoices.stats` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:read` — Read purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.stats Aggregated KPIs for your purchase invoices: total count and amount, counts per status, pending and overdue totals, and amounts by supplier. Returned as `{ "data": PurchaseInvoiceStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoiceStats), required) — Aggregated metrics (KPIs) of the purchase invoices of the authenticated company: total, count by status and accumulated amounts. `overdue` is a derived condition (`pending` + past due date), not a persisted status. - `object` (string, required, enum: `purchase_invoice_stats`) - `total_invoices` (integer, required) — Total number of purchase invoices. - `by_status` (object, required) — Invoice count by status (key = status, value = number of invoices). Keys: `draft`, `pending`, `paid`, `cancelled` (persisted statuses) plus `overdue` (derived KPI: overdue subset of `pending`). - `pending` (integer, required) — Number of invoices pending payment. - `draft` (integer, required) — Number of draft invoices. - `paid` (integer, required) — Number of paid invoices. - `cancelled` (integer, required) — Number of cancelled invoices. - `overdue` (integer, required) — Number of overdue and unpaid invoices (subset of `pending` with a past due date). - `total_amount` (number, required) — Total aggregate amount of all purchase invoices. - `pending_amount` (number, required) — Total amount pending payment. - `paid_amount` (number, required) — Total amount already paid. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/purchase_invoices/{purchase_invoice} — Update a purchase invoice - **Operation ID**: `public-api.v1.purchase_invoices.update` - **Tag**: Purchase Invoices - **Required scope**: `purchase_invoices:write` — Create and update purchase invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/purchase-invoices/public-api.v1.purchase_invoices.update Update a purchase invoice. ## Path parameters - `purchase_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 23 properties; none of them required. - `supplier_id` (string, optional, format: uuid) - `expense_category_id` (string | null, optional, format: uuid) - `external_invoice_number` (string, optional, maxLength 255) - `external_id` (string | null, optional, maxLength 100) — External business key (integration key from your ERP/CRM). Partial update: an explicit `external_id: null` clears the key. Orthogonal to `external_invoice_number`. - `internal_code` (string | null, optional, maxLength 255) - `issued_on` (string, optional, format: date) - `received_on` (string | null, optional, format: date) - `due_on` (string | null, optional, format: date) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `internal_notes` (string | null, optional, maxLength 2000) — Extend fields. Basic SHAPE only; the `payment_method` allowlist, `tax_period` format and `tags` cardinality are validated by the VO/Aggregate. - `payment_method` (string | null, optional, maxLength 30) - `payment_terms_days` (integer | null, optional, min 0, max 365) - `bank_account_id` (integer | null, optional) - `expense_account` (string | null, optional, maxLength 50) - `tax_period` (string | null, optional, maxLength 10) - `is_reverse_charge` (boolean | null, optional) - `deductible_percentage` (number | null, optional, min 0, max 100) - `operation_class` (string | null, optional, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) - `exclude_347` (boolean, optional) - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) — Typed custom fields as `[{field, value}]`. Partial update: an explicit `custom_fields: null` empties the collection. - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional, min 0.01) - `unit_price` (number, optional, min 0) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) — Per-line IRPF retention and deductible VAT. Same shape rules as create. - `vat_deductible` (boolean | null, optional) - `surcharge_rate` (number | null, optional, min 0, max 100) — Per-line equivalence surcharge (the legal VAT↔surcharge pair is validated) and LIVA exemption reason (closed catalog). - `exemption_reason` (string | null, optional, enum: `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) — Per-line indirect tax regime of the supplier. Same shape rule as create (defense in depth); the real invariant lives in the domain. ## Responses - **200** - Body (`application/json`): - `data` (object (PurchaseInvoice), required) — An invoice received from a supplier. - `id` (string, required) - `object` (string, required, enum: `purchase_invoice`) - `external_invoice_number` (string | null, required) — The number assigned by the supplier on their invoice. `null` for a simplified expense ticket (`is_simplified`) with no supplier number. - `is_simplified` (boolean, required) — Whether this is a simplified purchase invoice (expense ticket). When `true`, `supplier` and `external_invoice_number` may be `null`. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `internal_code` (string | null, required) — Optional internal code assigned by the company for its own classification. - `supplier` (object (SupplierRef) | null, required) - `status` (string, required) - `issued_on` (string, required, format: date) - `received_on` (string | null, required, format: date) — Reception date of the invoice, or `null`. - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total_retention` (number, required) — Aggregated IRPF withholding of the lines (Σ retention_amount). Header invariant: `total === subtotal + taxes_total − total_retention`. - `total` (number, required) - `currency` (string, required) - `paid_amount` (number, required) — Amount already paid against this purchase invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending payment for this purchase invoice (derived from the payment ledger). - `payment_status` (string, required, enum: `pending`, `partially_paid`, `paid`, `overdue`) — Derived payment status, NOT a persisted domain state (the model keeps 4 statuses). `overdue` derives from `pending` + `due_date < today` and prevails in presentation. - `paid_at` (string | null, required, format: date) - `payment_method` (string | null, required) - `payment_terms_days` (integer | null, required) — Payment term days agreed with the supplier, or `null`. - `bank_account` (null, required) — Supplier bank account. Always `null` in this version (accounts live embedded as JSON in `suppliers` without a stable identity). Follow-up `bridge-bank-account-cross-bc-supplier` will expose it as an object once Supplier promotes a read port. - `expense_account` (string | null, required) — Cuenta contable de gasto asociada, o `null`. - `expense_category_id` (string | null, required) — UUID (v7) of the associated expense category, or `null`. - `deductible_percentage` (number | null, required, format: float) — Deductible percentage of the input VAT (0–100), or `null` when not set. - `operation_class` (string, required, enum: `corriente`, `bien_inversion`, `importacion`, `intracomunitaria`) — Operation class for the input VAT of Modelo 303. Defaults to `corriente`. - `exclude_347` (boolean, required) — Declarative per-document flag: whether this purchase invoice is excluded from the annual Modelo 347 report. - `tax_period` (string | null, required) — Fiscal allocation period (format `YYYY-MM` or `YYYY-QN`), or `null`. - `is_reverse_charge` (boolean, required) — Indicates whether the invoice is subject to reverse charge. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `internal_notes` (string | null, required) — Internal notes not visible to the supplier, or `null`. - `attachment` (object (PurchaseInvoiceAttachment) | null, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `notes` (string | null, required) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The purchase invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-received invoice as received), an attempt to delete a paid purchase invoice, or a reused idempotency key. - **422** — Validation failed, or the purchase invoice cannot undergo the requested state transition (e.g. marking an already-received invoice as received). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/{quote}/accept — Accept a quote - **Operation ID**: `public-api.v1.quotes.accept` - **Tag**: Quotes - **Required scope**: `quotes:transition` — Change the lifecycle status of quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.accept Mark a quote as accepted by the client. Sets `accepted_at` to the current timestamp. ## Path parameters - `quote` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 2 properties; none of them required. Public REST API v1 — POST /v1/quotes/{uuid}/accept. Optional body: `accepted_on` (date, defaults to today), `notes`. The controller performs the cross-field validation for `quote_expired` (`valid_until < today` → 422). - `accepted_on` (string | null, optional, format: date) - `notes` (string | null, optional, maxLength 1000) ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/bulk-delete — Bulk delete quotes - **Operation ID**: `public-api.v1.quotes.bulk_delete` - **Tag**: Quotes - **Required scope**: `quotes:delete` — Delete quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.bulk_delete Deletes up to 100 quotes in one call. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each entry that could not be deleted. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Public REST API v1 — DELETE /v1/quotes/bulk. - `ids` (array, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/bulk-pdf — Bulk download quote PDFs - **Operation ID**: `public-api.v1.quotes.bulk_pdf` - **Tag**: Quotes - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.bulk_pdf Packages the PDFs of up to 50 quotes (by id) into a single ZIP. Ids that are not found or have no generable PDF do not abort the request: the ZIP carries only the valid ones and the per-resource counts travel in the `X-Bulk-*` response headers. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Package the PDFs of several quotes into a single ZIP. `ids` is an array of quote UUIDs, up to 50 per request. - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/zip`): - string - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/bulk-send — Bulk send quotes - **Operation ID**: `public-api.v1.quotes.bulk_send` - **Tag**: Quotes - **Required scope**: `quotes:send` — Send by email quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.bulk_send Sends up to 200 quotes by email (queued) in one call, reusing the single-send path per id. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`) for each quote that could not be sent (not found, terminal status or no resolvable recipient). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 1 required: `ids`. Email several quotes in one request (queued), up to 200 per batch. `ids` is an array of quote UUIDs; the optional `to`/`cc` arrays and `subject`/`message`/`language` overrides apply to the whole batch (when `to` is omitted, each quote uses its client email). - `subject` (string | null, optional, maxLength 200) - `message` (string | null, optional, maxLength 5000) - `language` (string | null, optional, maxLength 5) - `ids` (array, required, maxItems 200) - `to` (array | null, optional) - `cc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/bulk-status — Bulk change quote status - **Operation ID**: `public-api.v1.quotes.bulk_status` - **Tag**: Quotes - **Required scope**: `quotes:transition` — Change the lifecycle status of quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.bulk_status Transition up to 50 quotes (by id) to a status from the closed set `[approved, rejected]`, each through the document state guard. Returns a `BulkPartialSuccessResult`; quotes whose transition is rejected (not found or not transitionable) come back in `failures[]`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `new_status`, `ids`. Transition several quotes to `new_status` (`approved` or `rejected`) in one request, up to 50 per batch. `ids` is an array of quote UUIDs; every transition passes the document state guard, and quotes that cannot transition are returned under `failures[]`. - `new_status` (string, required, enum: `approved`, `rejected`) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/{quote}/convert — Convert quote to invoice - **Operation ID**: `public-api.v1.quotes.convert` - **Tag**: Quotes - **Required scope**: `quotes:transition` — Change the lifecycle status of quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.convert Convert an accepted quote into a sales invoice. The new invoice references the source quote via metadata; the quote moves to status `converted`. ## Path parameters - `quote` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 1 required: `target`. - `target` (string, required, enum: `invoice`, `proforma`, `delivery_note`) - `issued_on` (string | null, optional, format: date) - `due_on` (string | null, optional, format: date) ## Responses - **201** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes — Create a quote - **Operation ID**: `public-api.v1.quotes.create` - **Tag**: Quotes - **Required scope**: `quotes:write` — Create and update quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.create Create a new sales quote in `draft` status. Quotes can later be converted to invoices via `POST /quotes/{quote}/convert`. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 12 properties; 4 required: `client_id`, `issued_on`, `valid_until`, `lines`. - `client_id` (string, required, format: uuid) - `series_id` (string | null, optional, format: uuid) - `issued_on` (string, required, format: date) - `valid_until` (string, required, format: date) - `notes` (string | null, optional, maxLength 1000) - `terms` (string | null, optional, maxLength 2000) - `external_id` (string | null, optional, maxLength 100) - `currency` (string | null, optional, enum: `EUR`) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, required) - `description` (string, required, maxLength 255) - `quantity` (number, required, min 0.01) - `unit_price` (number, required, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **201** — Quote created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/quotes/{quote} — Delete a quote - **Operation ID**: `public-api.v1.quotes.delete` - **Tag**: Quotes - **Required scope**: `quotes:delete` — Delete quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.delete Delete a quote. Returns 422 if the quote has been converted to an invoice. ## Path parameters - `quote` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/{quote}/duplicate — Duplicate a quote - **Operation ID**: `public-api.v1.quotes.duplicate` - **Tag**: Quotes - **Required scope**: `quotes:write` — Create and update quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.duplicate Create a new draft quote by copying the lines, client, and metadata from an existing quote. ## Path parameters - `quote` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/find-by-external-id — Find a quote by external ID - **Operation ID**: `public-api.v1.quotes.find_by_external_id` - **Tag**: Quotes - **Required scope**: `quotes:read` — Read quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.find_by_external_id Look up a single quote by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Returns the matching quote or 404 `quote_not_found` if no quote uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a quote by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/quotes — List all quotes - **Operation ID**: `public-api.v1.quotes.list` - **Tag**: Quotes - **Required scope**: `quotes:read` — Read quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.list List your sales quotes with cursor-based pagination. Supports filtering by `status[in]`, `client_id`, `issued_on[gte|lte]`. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Quote status. - `status[in]` (string, optional) — Quote status. - `client_id` (string, optional, format: uuid) — Client ID (UUID v7). - `client_id[in]` (string, optional) — Client ID (UUID v7). - `series_id` (string, optional, format: uuid) — Series ID (UUID v7). - `series_id[in]` (string, optional) — Series ID (UUID v7). - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `valid_until[gte]` (string, optional, format: date) — Validity date. - `valid_until[lte]` (string, optional, format: date) — Validity date. - `valid_until[gt]` (string, optional, format: date) — Validity date. - `valid_until[lt]` (string, optional, format: date) — Validity date. - `total[gte]` (number, optional) — Total amount. - `total[lte]` (number, optional) — Total amount. - `total[gt]` (number, optional) — Total amount. - `total[lt]` (number, optional) — Total amount. - `number` (string, optional) — Quote number. - `number[contains]` (string, optional) — Quote number. - `tags` (string, optional) — Filter by classification tag (lowercase slug). - `tags[in]` (string, optional) — Filter by classification tag (lowercase slug). - `sort` (string, optional, enum: `created`, `-created`, `total`, `-total`, `number`, `-number`, `valid_until`, `-valid_until`) — Sort order. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/quotes/{quote}/pdf — Download quote PDF - **Operation ID**: `public-api.v1.quotes.pdf` - **Tag**: Quotes - **Required scope**: `pdfs:read` — Read pdfs. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.pdf Download the PDF representation of a quote. ## Path parameters - `quote` (string, required) ## Query parameters - `download` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - string - **304** - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/quotes/{quote}/public-link — Retrieve quote public link - **Operation ID**: `public-api.v1.quotes.public_link_get` - **Tag**: Quotes - **Required scope**: `quotes:read` — Read quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.public_link_get Returns the shareable public URL of the quote (/d/{uuid}) along with its status, expiration, and the plan-allowed maximum extension days. ## Path parameters - `quote` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/quotes/{quote}/public-link — Update quote public link - **Operation ID**: `public-api.v1.quotes.public_link_update` - **Tag**: Quotes - **Required scope**: `quotes:write` — Create and update quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.public_link_update Applies an action to the public link: `revoke`, `activate`, `extend` (with `extend_days`), or `reset` to the plan default. ## Path parameters - `quote` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `action`. Public REST API v1 — PUT /v1/quotes/{uuid}/public-link. `SchemaName` disambiguates the OpenAPI schema: Quote and Proforma declare structurally identical request bodies, so without a unique name they would collide in `components.schemas`. - `action` (string, required, enum: `revoke`, `activate`, `extend`, `reset`) - `extend_days` (integer, optional, min 1, max 36500) ## Responses - **200** - Body (`application/json`): - `data` (object (PublicLink), required) — Represents the state of the shareable public link of a document (quote/invoice/proforma/delivery_note). `url` is the absolute URL ready to send to the client; `enabled` indicates whether it is active; `expires_at` the deadline (`null` = unlimited); `max_days` the maximum allowed when extending it. - `object` (string, required, enum: `public_link`) - `url` (string, required, format: uri) — Absolute URL of the public link to share with the client. - `id` (string, required) — UUID (v7) of the document the link points to. - `enabled` (boolean, required) — Indicates whether the public link is currently active. - `expires_at` (string | null, required, format: date-time) — Expiration date/time of the link, or `null` if it does not expire. - `max_days` (integer, required) — Maximum number of days allowed when extending the link validity (business limit). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/{quote}/reject — Reject a quote - **Operation ID**: `public-api.v1.quotes.reject` - **Tag**: Quotes - **Required scope**: `quotes:transition` — Change the lifecycle status of quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.reject Mark a quote as rejected by the client. Sets `rejected_at` to the current timestamp. ## Path parameters - `quote` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. - `reason` (string | null, optional, maxLength 500) ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/quotes/{quote}/send — Send quote by email - **Operation ID**: `public-api.v1.quotes.send` - **Tag**: Quotes - **Required scope**: `quotes:send` — Send by email quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.send Send a quote to the client by email. ## Path parameters - `quote` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 5 properties; none of them required. Public REST API v1 — POST /v1/quotes/{uuid}/send. Optional body: `to` (string), `cc[]`, `bcc[]` (arrays of emails), `subject` (max 200), `body` (string). The controller performs the cross-field validation: if the client has no email and `to` is absent, it returns 422 `missing_required_param`. - `to` (string | null, optional, format: email, maxLength 191) - `subject` (string | null, optional, maxLength 200) - `body` (string | null, optional, maxLength 5000) - `cc` (array | null, optional) - `bcc` (array | null, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/quotes/{quote} — Retrieve a quote - **Operation ID**: `public-api.v1.quotes.show` - **Tag**: Quotes - **Required scope**: `quotes:read` — Read quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.show Retrieve a sales quote by its `uuid`. ## Path parameters - `quote` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/quotes/stats — Get quote stats - **Operation ID**: `public-api.v1.quotes.stats` - **Tag**: Quotes - **Required scope**: `quotes:read` — Read quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.stats Aggregated KPIs for the authenticated company: total quote count and amount, count per status, expired count, and converted count. Returned as `{ "data": QuoteStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (QuoteStats), required) — Resumen agregado de los presupuestos de la empresa autenticada: total, importe acumulado, conteo por estado, expirados y convertidos a factura. Devuelto por `GET /v1/quotes/stats`. - `total_count` (integer, required) — Total number of recorded quotes. - `total_amount` (number, required) — Aggregate amount of the quotes (EUR). - `count_by_status` (object, required) — Quote count by status. Keys: `draft`, `sent`, `accepted`, `rejected`, `expired`, `converted`, `cancelled`. - `expired_count` (integer, required) — Quotes whose `valid_until` date has already passed. - `converted_count` (integer, required) — Quotes converted to an invoice. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/quotes/statuses — List quote statuses - **Operation ID**: `public-api.v1.quotes.statuses` - **Tag**: Quotes - **Required scope**: `quotes:read` — Read quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.statuses Returns the canonical list of quote statuses available in the API along with their human-readable label and UI color. Useful for building dropdowns and filters. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/quotes/{quote} — Update a quote - **Operation ID**: `public-api.v1.quotes.update` - **Tag**: Quotes - **Required scope**: `quotes:write` — Create and update quotes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/quotes/public-api.v1.quotes.update Update a draft quote. Once accepted/rejected/converted, the quote becomes immutable. ## Path parameters - `quote` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 10 properties; none of them required. Public REST API v1 — PUT /v1/quotes/{uuid}. Partial update: omitted fields are kept. Only allowed when the quote is in `draft` status (the controller maps the transition exception to 422 `invalid_status_transition`). - `client_id` (string, optional, format: uuid) - `issued_on` (string, optional, format: date) - `valid_until` (string, optional, format: date) - `notes` (string | null, optional, maxLength 1000) - `terms` (string | null, optional, maxLength 2000) - `external_id` (string | null, optional, maxLength 100) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional, min 0.01) - `unit_price` (number, optional, min 0) - `tax_rate_id` (string | null, optional, format: uuid) - `tax_rate` (number | null, optional, min 0, max 100) - `retention_rate` (number | null, optional, min 0, max 100) - `surcharge_rate` (number | null, optional, min 0, max 100) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `product_id` (string | null, optional, format: uuid) - `discount_percent` (number | null, optional, min 0, max 100) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **200** - Body (`application/json`): - `data` (object (Quote), required) — A sales quote that can be converted to an invoice. - `id` (string, required) - `object` (string, required, enum: `quote`) - `number` (string, required) - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Quote lifecycle status (draft, sent, accepted, rejected, expired, converted, cancelled). - `issued_on` (string, required, format: date) - `valid_until` (string | null, required, format: date) - `accepted_at` (string | null, required, format: date-time) - `rejected_at` (string | null, required, format: date-time) - `converted_to_id` (string | null, required) — UUID (v7) of the invoice this quote was converted into, if any. - `converted_invoice_number` (string | null, required) — Human-readable number of the invoice this quote was converted into (e.g. "F-2026-00042"). null if not converted. - `subtotal` (number, required) - `taxes_total` (number, required) — Aggregated tax amount. Use total_vat/total_retention/total_surcharge for breakdown. - `total_vat` (number, required) — Sum of VAT (IVA) across all lines. - `total_retention` (number, required) — Sum of withholding (IRPF/retention) across all lines. - `total_surcharge` (number, required) — Sum of equivalence surcharge (recargo de equivalencia) across all lines. - `total` (number, required) - `currency` (string, required) - `notes` (string | null, required) - `terms` (string | null, required) — Free-text terms and conditions rendered on the quote PDF. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `link_expires_at` (string | null, required, format: date-time) — Expiry date of the public share link, or null if unlimited. - `link_is_active` (boolean, required) — Whether the public share link is currently active. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The quote request conflicts with its current state — e.g. an invalid status transition (accepting a rejected quote), an attempt to convert a non-accepted quote, or a reused idempotency key. - **422** — Validation failed, or the quote cannot undergo the requested state transition (e.g. accepting an already-accepted quote, or converting a non-accepted quote). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/{recurring_invoice}/activate — Activate recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.activate` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:transition` — Change the lifecycle status of recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.activate Activate a paused recurring invoice. The next invoice will be generated according to the schedule. This is a semantic alias of `POST /recurring_invoices/{recurring_invoice}/resume` — both map to the same handler and behave identically; neither is deprecated. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/recurring_invoices/{recurring_invoice}/activities — List recurring invoice activity - **Operation ID**: `public-api.v1.recurring_invoices.activities` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.activities Return the cursor-paginated activity timeline (domain events: activation, pause, resume, generation, failure, cancellation, etc.) for a recurring invoice. Metadata is sanitized to never expose internal identifiers. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `recurring_invoice.activated`, `recurring_invoice.executed`, `recurring_invoice.cancelled`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed, or the recurring invoice cannot undergo the requested state transition (e.g. resuming a recurrence that is not paused). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/bulk-delete — Bulk delete recurring invoices - **Operation ID**: `public-api.v1.recurring_invoices.bulk-delete` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:delete` — Delete recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.bulk-delete Delete multiple recurring invoices in a single request (POST with a body of `ids`). Returns the count of deleted resources and a list of failures with their reason. Recurring invoices that already generated invoices cannot be deleted. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Delete several recurring invoices in one request. `ids` is an array of 1 to 100 UUIDs; unknown or cross-tenant identifiers are reported under `failed`. - `ids` (array, required, maxItems 100) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed, or the recurring invoice cannot undergo the requested state transition (e.g. resuming a recurrence that is not paused). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/{recurring_invoice}/cancel — Cancel recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.cancel` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:transition` — Change the lifecycle status of recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.cancel Cancel a recurring invoice. Unlike `pause`, this is a terminal, irreversible state: a cancelled recurring invoice can never be resumed or reactivated. Previously generated invoices are unaffected. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices — Create a recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.create` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:write` — Create and update recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.create Create a recurring invoice template that auto-generates invoices on a fixed cadence (weekly, monthly, quarterly, yearly). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 19 properties; 4 required: `client_id`, `frequency`, `start_on`, `lines`. Create a recurring invoice template that auto-generates invoices on a fixed cadence. Required: `client_id`, `series_id`, `frequency`, `start_on` and `lines[]` (at least one). Optional: `end_on`, `name`, `description`, `notes`, `metadata`, `holiday_handling`, `days_before_due`, `max_occurrences`, `email_to`, `send_automatically`, `tags` and `custom_fields`. `frequency` accepts `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual` or `yearly`. - `client_id` (string, required, format: uuid) - `series_id` (string | null, optional, format: uuid) - `name` (string | null, optional, maxLength 255) - `description` (string | null, optional, maxLength 1000) - `frequency` (string, required, enum: `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual`, `yearly`) - `start_on` (string, required, format: date) - `end_on` (string | null, optional, format: date) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) - `holiday_handling` (string, optional) - `days_before_due` (integer | null, optional, min 0) - `max_occurrences` (integer | null, optional, min 1) - `email_to` (string | null, optional, format: email) - `send_automatically` (boolean, optional) - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `auto_delivery` (object, optional) - `send_automatically` (boolean | null, optional) - `recipients` (array | null, optional) - `cc` (array | null, optional) - `subject` (string | null, optional, maxLength 255) - `body` (string | null, optional, maxLength 5000) - `lines` (array, required) - `description` (string, required, maxLength 255) - `quantity` (number, required, min 0.01) - `unit_price` (number, required, min 0) - `tax_rate` (number | null, optional, min 0, max 100) - `retention` (number | null, optional, min 0, max 100) - `surcharge` (number | null, optional, min 0, max 100) - `exemption_reason` (string | null, optional, enum: `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`) - `regime_key` (string | null, optional, enum: `01`, `02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`, `14`, `15`, `17`, `18`, `19`, `20`) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **201** — Recurring invoice created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed, or the recurring invoice cannot undergo the requested state transition (e.g. resuming a recurrence that is not paused). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/recurring_invoices/{recurring_invoice} — Delete a recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.delete` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:delete` — Delete recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.delete Delete a recurring invoice template. Future invoices stop being generated; existing invoices remain. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/find-by-external-id — Find a recurring invoice by external ID - **Operation ID**: `public-api.v1.recurring_invoices.find_by_external_id` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.find_by_external_id Look up a single recurring invoice template by its `external_id` (sent in the JSON body), the integration key that maps it to a record in a third-party system (ERP/CRM/e-commerce). Returns the matching recurring invoice or 404 `recurring_invoice_not_found` if none uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a recurring invoice by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed, or the recurring invoice cannot undergo the requested state transition (e.g. resuming a recurrence that is not paused). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/{recurring_invoice}/generate — Generate an invoice from a recurring template - **Operation ID**: `public-api.v1.recurring_invoices.generate` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:write` — Create and update recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.generate Trigger immediate invoice generation from the recurring configuration, outside the scheduled cycle. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `object` (string, required, const: `recurring_invoice.generate_result`) - `invoice_id` (string | null, required) - `invoice_number` (string | null, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/recurring_invoices — List all recurring invoices - **Operation ID**: `public-api.v1.recurring_invoices.list` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.list List your recurring invoice templates with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Recurring plan status. - `status[in]` (string, optional) — Recurring plan status. - `client_id` (string, optional, format: uuid) — Client ID (UUID v7). - `client_id[in]` (string, optional) — Client ID (UUID v7). - `frequency` (string, optional) — Issuance frequency (e.g. monthly, yearly). - `frequency[in]` (string, optional) — Issuance frequency (e.g. monthly, yearly). - `next_run_at[gte]` (string, optional, format: date-time) — Next run date. - `next_run_at[lte]` (string, optional, format: date-time) — Next run date. - `next_run_at[gt]` (string, optional, format: date-time) — Next run date. - `next_run_at[lt]` (string, optional, format: date-time) — Next run date. - `name` (string, optional) — Name of the recurring plan. - `name[contains]` (string, optional) — Name of the recurring plan. - `tags` (string, optional) — Filter by classification tag (lowercase slug). - `tags[in]` (string, optional) — Filter by classification tag (lowercase slug). - `sort` (string, optional, enum: `created`, `-created`, `next_run_at`, `-next_run_at`) — Sort order. - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — Non-existent or cross-company public FK (`client_uuid`/`series_uuid`) → empty result without leaking cross-tenant existence. - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed, or the recurring invoice cannot undergo the requested state transition (e.g. resuming a recurrence that is not paused). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/recurring_invoices/{recurring_invoice}/logs — List recurring invoice execution logs - **Operation ID**: `public-api.v1.recurring_invoices.logs` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.logs Return paginated history of generations, failures and other events for this recurring template. ## Path parameters - `recurring_invoice` (string, required) ## Query parameters - `per_page` (string, optional, default: `"25"`) - `limit` (string, optional) — Default `'25'` (string) por consistencia OpenAPI/Spectral. - `cursor` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `has_more` (string, required) - `next_cursor` (string | null, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/{recurring_invoice}/pause — Pause recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.pause` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:transition` — Change the lifecycle status of recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.pause Pause a recurring invoice. No new invoices will be generated until resumed. Reversible — use `resume`/`activate` to reactivate. For a permanent, irreversible stop use `cancel`. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/recurring_invoices/{recurring_invoice}/preview — Preview upcoming recurring invoice dates - **Operation ID**: `public-api.v1.recurring_invoices.preview` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.preview Return the next scheduled run dates with their due dates and estimated totals. Defaults to 5 occurrences. ## Path parameters - `recurring_invoice` (string, required) ## Query parameters - `count` (string, optional) - `expand` (string, optional) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - object - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/{recurring_invoice}/resume — Resume recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.resume` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:transition` — Change the lifecycle status of recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.resume Resume a paused recurring invoice. This is a semantic alias of `POST /recurring_invoices/{recurring_invoice}/activate` — both map to the same handler and behave identically; neither is deprecated. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/recurring_invoices/{recurring_invoice} — Retrieve a recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.show` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.show Retrieve a recurring invoice template by its `uuid`. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/recurring_invoices/{recurring_invoice}/skip — Skip the next recurring invoice generation - **Operation ID**: `public-api.v1.recurring_invoices.skip` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:write` — Create and update recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.skip Advance the recurring invoice to its following scheduled run without generating an invoice for the current cycle. The skipped occurrence is not counted against any occurrence limit. Cancelled or completed recurring invoices return 422. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/recurring_invoices/stats — Retrieve recurring invoice stats - **Operation ID**: `public-api.v1.recurring_invoices.stats` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:read` — Read recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.stats Aggregate KPIs for your recurring invoices: counts by status, due today / this week, generated and failed this month, breakdown by frequency, next scheduled runs and estimated revenue this month. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoiceStats), required) — Aggregated summary of the recurring invoices of the authenticated company: counters by status, upcoming due dates, current-month generation/failures, breakdown by frequency, upcoming runs and estimated revenue. Returned by `GET /v1/recurring_invoices/stats`. - `object` (string, required, enum: `recurring_invoice_stats`) - `total` (integer, required) — Total de recurrencias registradas. - `active` (integer, required) — Active recurrences (generating invoices). - `paused` (integer, required) — Recurrencias pausadas (reanudables). - `cancelled` (integer, required) — Recurrencias canceladas (estado terminal irreversible). - `completed` (integer, required) — Completed recurrences (reached `max_occurrences`). Reported as `0` until the repository provides the dedicated count. - `due_today` (integer, required) — Recurrences whose next run is today. - `due_this_week` (integer, required) — Recurrences whose next run falls in the current week. - `generated_this_month` (integer, required) — Invoices generated by recurrences during the current month. - `failed_this_month` (integer, required) — Ejecuciones de recurrencia fallidas durante el mes en curso. - `frequency_breakdown` (object, required) — Recurrence count by frequency. Keys: `weekly`, `monthly`, `quarterly`, `yearly`, etc. - `next_scheduled` (array, required) — Upcoming recurrences to run, ordered by run date ascending. - `estimated_revenue_this_month` (number, required) — Estimated revenue from recurrences during the current month (EUR). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/recurring_invoices/{recurring_invoice} — Update a recurring invoice - **Operation ID**: `public-api.v1.recurring_invoices.update` - **Tag**: Recurring Invoices - **Required scope**: `recurring_invoices:write` — Create and update recurring invoices. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/recurring-invoices/public-api.v1.recurring_invoices.update Update a recurring invoice template. The cadence and lines apply to invoices generated after the update; previously generated invoices are unaffected. ## Path parameters - `recurring_invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 19 properties; none of them required. Partial update of a recurring invoice template; only the fields sent overwrite the current value, omitted ones are preserved. Writable fields mirror creation (`client_id`, `series_id`, `name`, `description`, `frequency`, `holiday_handling`, `start_on`, `end_on`, `notes`, `metadata`, `days_before_due`, `max_occurrences`, `email_to`, `send_automatically`, `lines[]`). - `client_id` (string, optional, format: uuid) - `series_id` (string | null, optional, format: uuid) - `name` (string | null, optional, maxLength 255) - `description` (string | null, optional, maxLength 1000) - `frequency` (string, optional, enum: `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual`, `yearly`) - `holiday_handling` (string, optional) - `start_on` (string, optional, format: date) - `end_on` (string | null, optional, format: date) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) - `days_before_due` (integer | null, optional, min 0) - `max_occurrences` (integer | null, optional, min 1) - `email_to` (string | null, optional, format: email) - `send_automatically` (boolean, optional) - `tags` (array | null, optional, maxItems 30) - `custom_fields` (array | null, optional, maxItems 50) - `field` (string, required, maxLength 60, minLength 1) - `value` (string, required, maxLength 500) - `auto_delivery` (object, optional) - `send_automatically` (boolean | null, optional) - `recipients` (array | null, optional) - `cc` (array | null, optional) - `subject` (string | null, optional, maxLength 255) - `body` (string | null, optional, maxLength 5000) - `lines` (array, optional) - `description` (string, optional, maxLength 255) - `quantity` (number, optional, min 0.01) - `unit_price` (number, optional, min 0) - `tax_rate` (number | null, optional, min 0, max 100) - `retention` (number | null, optional, min 0, max 100) - `surcharge` (number | null, optional, min 0, max 100) - `exemption_reason` (string | null, optional, enum: `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`) - `regime_key` (string | null, optional, enum: `01`, `02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`, `14`, `15`, `17`, `18`, `19`, `20`) - `retention_rate_id` (string | null, optional, format: uuid) - `surcharge_rate_id` (string | null, optional, format: uuid) - `indirect_tax_regime` (string | null, optional, enum: `iva`, `igic`, `ipsi`) ## Responses - **200** - Body (`application/json`): - `data` (object (RecurringInvoice), required) — A recurring invoice template that auto-generates invoices on a fixed cadence. - `id` (string, required) - `object` (string, required, enum: `recurring_invoice`) - `client` (object (ClientRef), required) - `series` (object (SeriesRef), required) - `status` (string, required) — Recurring schedule status (active, paused, completed, cancelled). - `frequency` (string, required) — Cadence (weekly, monthly, quarterly, yearly, etc.). - `name` (string, required) — Descriptive name of the recurrence. - `description` (string | null, required) — Free-text description of the recurrence. - `notes` (string | null, required) — Internal notes carried over to the generated invoices. - `email_to` (string | null, required, format: email) — Recipient of the automatic dispatch of generated invoices. `null` if not configured. - `send_automatically` (boolean, required) — If `true`, the generated invoices are automatically emailed to `email_to`. - `days_before_due` (integer, required) — Payment term days (Net X) applied to the due date of each generated invoice. - `max_occurrences` (integer | null, required) — Maximum number of invoices to generate before completing the recurrence. `null` = no limit. - `occurrences_count` (integer, required) — Number of invoices already generated by this recurrence. - `remaining_occurrences` (integer | null, required) — Invoices remaining until `max_occurrences` is reached. `null` when there is no limit. - `holiday_handling` (string, required) — Adjustment policy when the run date falls on a holiday/weekend (e.g. `none`, `next_business_day`, `previous_business_day`). - `start_on` (string, required, format: date) - `end_on` (string | null, required, format: date) - `next_run_at` (string, required) — When the next invoice will be generated. - `last_run_at` (string | null, required) — When the last invoice was generated, if any. - `cancelled_at` (string | null, required, format: date-time) — When the recurrence was cancelled (irreversible terminal state). `null` if not cancelled. - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `currency` (string, required) - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `auto_delivery` (object, required) — Rich auto-delivery configuration (superset of the scalar `email_to`): the generated invoices are emailed to `recipients` (with optional `cc`) using `subject`/`body`. `recipients`/`cc` are empty lists and `subject`/`body` are `null` when nothing is configured. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed, or the recurring invoice cannot undergo the requested state transition (e.g. resuming a recurrence that is not paused). - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/series/active — List active series by document type - **Operation ID**: `public-api.v1.series.active` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.active Return all non-archived series for the given document type within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Series), required) — A document numbering series. Immutable per AEAT compliance. - `id` (string, required) - `object` (string, required, enum: `series`) - `code` (string, required) - `name` (string, required) - `document_type` (string, required) — invoice, quote, proforma, delivery_note, etc. - `prefix` (string, required) — Legacy alias for code. Use code field. Will be removed in v2. - `number_format` (string, required) — Canonical numbering mask describing how the document number is rendered: padding (the block of zeros), year token (`{YYYY}` 4 digits / `{YY}` 2 digits / omitted for no year), optional month token (`{MM}` 2 digits) and separator. Example: `{code}-{YYYY}-{000}` (default) or `{code}-{YYYY}-{00000}` for 5-digit padding. When `counter_reset` is `monthly`, the mask must include the `{MM}` token (e.g. `F-{YYYY}-{MM}-{000}`) so the rendered number stays unique across months; otherwise two months would both start at `1`. Set on creation and immutable afterwards. Mirrors Holded's `format`. - `next_number` (integer, required) — Next correlative that will be assigned on the next real emission. - `current_number` (integer, required) — Last correlative actually emitted in the current fiscal year (0 for a brand-new series). - `initial_number` (integer, required) — Number the counter starts from. Set on creation to continue an existing numbering when migrating (e.g. 235). Defaults to 1. - `counter_reset` (string, required, enum: `never`, `annual`, `monthly`) — Counter reset policy (source of truth): `never` (the counter never resets), `annual` (resets on January 1st) or `monthly` (resets on the 1st of each month). `monthly` requires the `number_format` mask to include the `{MM}` token to keep rendered numbers unique across months. - `year_reset` (boolean, required, DEPRECATED) — Deprecated alias of `counter_reset`. `true` is equivalent to `counter_reset: "annual"`, `false` to `counter_reset: "never"`. Use `counter_reset` instead; this field will be removed in v2. - `is_default` (boolean, required) - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/series/{series}/activities — List series activity timeline - **Operation ID**: `public-api.v1.series.activities` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.activities Return the audit timeline for a series combining its own domain events (creation, archive/unarchive, default changes, number consumption). Paginated with a page-number cursor (`starting_after` is the next page number). ## Path parameters - `series` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `series.created`, `series.archived`, `series.number_consumed`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/series/{series}/archive — Archive a series - **Operation ID**: `public-api.v1.series.archive` - **Tag**: Series - **Required scope**: `series:write` — Create and update series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.archive Archive a series so it stops appearing as available for new documents. Fails with 409 if the series is the default and the only active series of its type. Returns 204 on success. ## Path parameters - `series` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/series/bootstrap — Bootstrap the default series of a company - **Operation ID**: `public-api.v1.series.bootstrap` - **Tag**: Series - **Required scope**: `series:write` — Create and update series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.bootstrap Leave a company able to issue documents in one call: for every document type of the public surface (`invoice`, `quote`, `delivery_note`, `proforma`) that has no active series, create its default series with the canonical code and name. No request body. **What comes back** - One entry per document type, with a `status` of `created`, `existing` or `no_default`. - `no_default` means the type has active series but none marked as default — archiving the default demotes it without promoting a replacement — and the company still cannot issue that document. - Treat `no_default` as work still to do, not as success: the active series arrive in `candidates` and you resolve it with `POST /v1/series/{id}/default`. **Why it does not choose for you** Picking which series numbers a company's documents has registry consequences only you can decide, so the bootstrap never promotes one for you. **Calling it twice** - Idempotent by business rule: a second call creates nothing, fails nothing and reports the state again. - INDEPENDENT of the `Idempotency-Key` header: with the header, a repeated key replays the original body — `created` entries included — instead of reporting the current state. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (SeriesBootstrapResult), required) — Result of bootstrapping the numbering of a company: one entry per document type of the public surface, always in the same order (`invoice`, `quote`, `delivery_note`, `proforma`). - `results` (array, required) — One entry per document type evaluated. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/series — Create a series - **Operation ID**: `public-api.v1.series.create` - **Tag**: Series - **Required scope**: `series:write` — Create and update series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.create Create a document numbering series. Optional `number_format` sets the numbering mask (e.g. `{code}-{YYYY}-{00000}`) and `initial_number` (≥1) starts the counter to continue an existing numbering. The same code may be reused across document types (multi-series). A series is immutable once created per AEAT (`PUT` returns 405), so these can only be set here. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 8 properties; 2 required: `code`, `document_type`. - `code` (string, required, maxLength 10) - `name` (string | null, optional, maxLength 255) - `document_type` (string, required, enum: `invoice`, `quote`, `delivery_note`, `proforma`) - `prefix` (string | null, optional, maxLength 20) - `counter_reset` (string | null, optional, enum: `never`, `annual`, `monthly`) - `year_reset` (boolean | null, optional) - `number_format` (string | null, optional, maxLength 32) - `initial_number` (integer | null, optional, min 1) ## Responses - **201** — Series created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Series), required) — A document numbering series. Immutable per AEAT compliance. - `id` (string, required) - `object` (string, required, enum: `series`) - `code` (string, required) - `name` (string, required) - `document_type` (string, required) — invoice, quote, proforma, delivery_note, etc. - `prefix` (string, required) — Legacy alias for code. Use code field. Will be removed in v2. - `number_format` (string, required) — Canonical numbering mask describing how the document number is rendered: padding (the block of zeros), year token (`{YYYY}` 4 digits / `{YY}` 2 digits / omitted for no year), optional month token (`{MM}` 2 digits) and separator. Example: `{code}-{YYYY}-{000}` (default) or `{code}-{YYYY}-{00000}` for 5-digit padding. When `counter_reset` is `monthly`, the mask must include the `{MM}` token (e.g. `F-{YYYY}-{MM}-{000}`) so the rendered number stays unique across months; otherwise two months would both start at `1`. Set on creation and immutable afterwards. Mirrors Holded's `format`. - `next_number` (integer, required) — Next correlative that will be assigned on the next real emission. - `current_number` (integer, required) — Last correlative actually emitted in the current fiscal year (0 for a brand-new series). - `initial_number` (integer, required) — Number the counter starts from. Set on creation to continue an existing numbering when migrating (e.g. 235). Defaults to 1. - `counter_reset` (string, required, enum: `never`, `annual`, `monthly`) — Counter reset policy (source of truth): `never` (the counter never resets), `annual` (resets on January 1st) or `monthly` (resets on the 1st of each month). `monthly` requires the `number_format` mask to include the `{MM}` token to keep rendered numbers unique across months. - `year_reset` (boolean, required, DEPRECATED) — Deprecated alias of `counter_reset`. `true` is equivalent to `counter_reset: "annual"`, `false` to `counter_reset: "never"`. Use `counter_reset` instead; this field will be removed in v2. - `is_default` (boolean, required) - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/series/default — Get the default series for a document type - **Operation ID**: `public-api.v1.series.default` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.default Return the default numbering series for the given document type (invoice, quote, proforma, delivery_note). Returns 404 when no default is configured. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Series), required) — A document numbering series. Immutable per AEAT compliance. - `id` (string, required) - `object` (string, required, enum: `series`) - `code` (string, required) - `name` (string, required) - `document_type` (string, required) — invoice, quote, proforma, delivery_note, etc. - `prefix` (string, required) — Legacy alias for code. Use code field. Will be removed in v2. - `number_format` (string, required) — Canonical numbering mask describing how the document number is rendered: padding (the block of zeros), year token (`{YYYY}` 4 digits / `{YY}` 2 digits / omitted for no year), optional month token (`{MM}` 2 digits) and separator. Example: `{code}-{YYYY}-{000}` (default) or `{code}-{YYYY}-{00000}` for 5-digit padding. When `counter_reset` is `monthly`, the mask must include the `{MM}` token (e.g. `F-{YYYY}-{MM}-{000}`) so the rendered number stays unique across months; otherwise two months would both start at `1`. Set on creation and immutable afterwards. Mirrors Holded's `format`. - `next_number` (integer, required) — Next correlative that will be assigned on the next real emission. - `current_number` (integer, required) — Last correlative actually emitted in the current fiscal year (0 for a brand-new series). - `initial_number` (integer, required) — Number the counter starts from. Set on creation to continue an existing numbering when migrating (e.g. 235). Defaults to 1. - `counter_reset` (string, required, enum: `never`, `annual`, `monthly`) — Counter reset policy (source of truth): `never` (the counter never resets), `annual` (resets on January 1st) or `monthly` (resets on the 1st of each month). `monthly` requires the `number_format` mask to include the `{MM}` token to keep rendered numbers unique across months. - `year_reset` (boolean, required, DEPRECATED) — Deprecated alias of `counter_reset`. `true` is equivalent to `counter_reset: "annual"`, `false` to `counter_reset: "never"`. Use `counter_reset` instead; this field will be removed in v2. - `is_default` (boolean, required) - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/series/find-by-code — Find a series by code - **Operation ID**: `public-api.v1.series.find_by_code` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.find_by_code Look up a series by its `code` (JSON body, case-insensitive). A `code` is not unique across document types (multi-series), so pass `document_type` to resolve the exact `(code, document_type)` series. If you omit it the code is matched across all types: a single match is returned, but an ambiguous code returns 422 `document_type_required_for_ambiguous_code` rather than silently picking one. Returns 404 if none exists. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `code`. Public REST API v1 — POST /v1/series/find-by-code. Looks up a series by its `code` (normalized to uppercase in the handler) within the authenticated company. The `code` travels in the JSON body (not in query params) because it is a private attribute that should not end up in proxy logs. `document_type` is optional and disambiguates matches when the same `code` is associated with several types. - `code` (string, required, maxLength 10) - `document_type` (string, optional, enum: `invoice`, `quote`, `delivery_note`, `proforma`) ## Responses - **200** - Body (`application/json`): - `data` (object (Series), required) — A document numbering series. Immutable per AEAT compliance. - `id` (string, required) - `object` (string, required, enum: `series`) - `code` (string, required) - `name` (string, required) - `document_type` (string, required) — invoice, quote, proforma, delivery_note, etc. - `prefix` (string, required) — Legacy alias for code. Use code field. Will be removed in v2. - `number_format` (string, required) — Canonical numbering mask describing how the document number is rendered: padding (the block of zeros), year token (`{YYYY}` 4 digits / `{YY}` 2 digits / omitted for no year), optional month token (`{MM}` 2 digits) and separator. Example: `{code}-{YYYY}-{000}` (default) or `{code}-{YYYY}-{00000}` for 5-digit padding. When `counter_reset` is `monthly`, the mask must include the `{MM}` token (e.g. `F-{YYYY}-{MM}-{000}`) so the rendered number stays unique across months; otherwise two months would both start at `1`. Set on creation and immutable afterwards. Mirrors Holded's `format`. - `next_number` (integer, required) — Next correlative that will be assigned on the next real emission. - `current_number` (integer, required) — Last correlative actually emitted in the current fiscal year (0 for a brand-new series). - `initial_number` (integer, required) — Number the counter starts from. Set on creation to continue an existing numbering when migrating (e.g. 235). Defaults to 1. - `counter_reset` (string, required, enum: `never`, `annual`, `monthly`) — Counter reset policy (source of truth): `never` (the counter never resets), `annual` (resets on January 1st) or `monthly` (resets on the 1st of each month). `monthly` requires the `number_format` mask to include the `{MM}` token to keep rendered numbers unique across months. - `year_reset` (boolean, required, DEPRECATED) — Deprecated alias of `counter_reset`. `true` is equivalent to `counter_reset: "annual"`, `false` to `counter_reset: "never"`. Use `counter_reset` instead; this field will be removed in v2. - `is_default` (boolean, required) - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/series — List all series - **Operation ID**: `public-api.v1.series.list` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.list List your document numbering series with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `document_type` (string, optional) — Document type (invoice, quote, etc.). - `document_type[in]` (string, optional) — Document type (invoice, quote, etc.). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `series`) - `code` (string, required) - `name` (string, required) - `document_type` (string, required) — invoice, quote, proforma, delivery_note, etc. - `prefix` (string, required) — Legacy alias for code. Use code field. Will be removed in v2. - `number_format` (string, required) — Canonical numbering mask describing how the document number is rendered: padding (the block of zeros), year token (`{YYYY}` 4 digits / `{YY}` 2 digits / omitted for no year), optional month token (`{MM}` 2 digits) and separator. Example: `{code}-{YYYY}-{000}` (default) or `{code}-{YYYY}-{00000}` for 5-digit padding. When `counter_reset` is `monthly`, the mask must include the `{MM}` token (e.g. `F-{YYYY}-{MM}-{000}`) so the rendered number stays unique across months; otherwise two months would both start at `1`. Set on creation and immutable afterwards. Mirrors Holded's `format`. - `next_number` (integer, required) — Next correlative that will be assigned on the next real emission. - `current_number` (integer, required) — Last correlative actually emitted in the current fiscal year (0 for a brand-new series). - `initial_number` (integer, required) — Number the counter starts from. Set on creation to continue an existing numbering when migrating (e.g. 235). Defaults to 1. - `counter_reset` (string, required, enum: `never`, `annual`, `monthly`) — Counter reset policy (source of truth): `never` (the counter never resets), `annual` (resets on January 1st) or `monthly` (resets on the 1st of each month). `monthly` requires the `number_format` mask to include the `{MM}` token to keep rendered numbers unique across months. - `year_reset` (boolean, required, DEPRECATED) — Deprecated alias of `counter_reset`. `true` is equivalent to `counter_reset: "annual"`, `false` to `counter_reset: "never"`. Use `counter_reset` instead; this field will be removed in v2. - `is_default` (boolean, required) - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/series/{series}/default — Mark a series as default for its type - **Operation ID**: `public-api.v1.series.set_default` - **Tag**: Series - **Required scope**: `series:write` — Create and update series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.set_default Promote a series to default for its document type. If another series was the default for the same type it is demoted atomically. Returns 204 on success. ## Path parameters - `series` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/series/{series} — Retrieve a series - **Operation ID**: `public-api.v1.series.show` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.show Retrieve a series by its `uuid`. Series are **immutable** for fiscal compliance (AEAT VeriFactu — legal numbering continuity): `PUT`, `PATCH` and `DELETE` on `/v1/series/{uuid}` return `405 Method Not Allowed` with `error.code = "series_immutable"` and header `Allow: GET, POST`. To "delete" a series use `POST /v1/series/{uuid}/archive`; to change the numbering, create a new series and mark it as default. ## Path parameters - `series` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Series), required) — A document numbering series. Immutable per AEAT compliance. - `id` (string, required) - `object` (string, required, enum: `series`) - `code` (string, required) - `name` (string, required) - `document_type` (string, required) — invoice, quote, proforma, delivery_note, etc. - `prefix` (string, required) — Legacy alias for code. Use code field. Will be removed in v2. - `number_format` (string, required) — Canonical numbering mask describing how the document number is rendered: padding (the block of zeros), year token (`{YYYY}` 4 digits / `{YY}` 2 digits / omitted for no year), optional month token (`{MM}` 2 digits) and separator. Example: `{code}-{YYYY}-{000}` (default) or `{code}-{YYYY}-{00000}` for 5-digit padding. When `counter_reset` is `monthly`, the mask must include the `{MM}` token (e.g. `F-{YYYY}-{MM}-{000}`) so the rendered number stays unique across months; otherwise two months would both start at `1`. Set on creation and immutable afterwards. Mirrors Holded's `format`. - `next_number` (integer, required) — Next correlative that will be assigned on the next real emission. - `current_number` (integer, required) — Last correlative actually emitted in the current fiscal year (0 for a brand-new series). - `initial_number` (integer, required) — Number the counter starts from. Set on creation to continue an existing numbering when migrating (e.g. 235). Defaults to 1. - `counter_reset` (string, required, enum: `never`, `annual`, `monthly`) — Counter reset policy (source of truth): `never` (the counter never resets), `annual` (resets on January 1st) or `monthly` (resets on the 1st of each month). `monthly` requires the `number_format` mask to include the `{MM}` token to keep rendered numbers unique across months. - `year_reset` (boolean, required, DEPRECATED) — Deprecated alias of `counter_reset`. `true` is equivalent to `counter_reset: "annual"`, `false` to `counter_reset: "never"`. Use `counter_reset` instead; this field will be removed in v2. - `is_default` (boolean, required) - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/series/stats — Get series stats - **Operation ID**: `public-api.v1.series.stats` - **Tag**: Series - **Required scope**: `series:read` — Read series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.stats Aggregated KPIs for your document numbering series: total series count, active and archived counts, and a breakdown by document type. Returned as `{ "data": SeriesStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `total` (integer, required) - `invoice` (integer, required) - `quote` (integer, required) - `delivery_note` (integer, required) - `proforma` (integer, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/series/{series}/unarchive — Unarchive a series - **Operation ID**: `public-api.v1.series.unarchive` - **Tag**: Series - **Required scope**: `series:write` — Create and update series. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/series/public-api.v1.series.unarchive Return an archived series back to the active pool. Does not change the current default of its type. Returns 204 on success. ## Path parameters - `series` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/payouts — List Stripe payouts - **Operation ID**: `public-api.v1.payouts.list` - **Tag**: Stripe - **Required scope**: `payouts:read` — Read payouts. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.payouts.list List the Stripe payouts ingested for your company, with cursor-based pagination. Each exposes the net/fees/gross amounts, currency, arrival date, reconciliation `status` (`ingested`/`reconciled`) and an informative `composition`. Filter by `status` and arrival-date window. Payouts are read-only; bank reconciliation happens in the dashboard. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Reconciliation status (ingested, reconciled). - `status[in]` (string, optional) — Reconciliation status (ingested, reconciled). - `arrival_date[gte]` (string, optional, format: date) — Expected arrival date of the payout (YYYY-MM-DD). - `arrival_date[lte]` (string, optional, format: date) — Expected arrival date of the payout (YYYY-MM-DD). - `arrival_date[gt]` (string, optional, format: date) — Expected arrival date of the payout (YYYY-MM-DD). - `arrival_date[lt]` (string, optional, format: date) — Expected arrival date of the payout (YYYY-MM-DD). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — UUID (v7) of the payout. Public identity (KEY `id`). - `object` (string, required, enum: `stripe_payout`) — Stripe-like discriminator. Always `stripe_payout` for this resource. - `connected_account_id` (string, required) — External Stripe connected account id (`acct_xxx`) the payout was paid out from. NOT an internal UUID nor a foreign key. - `stripe_payout_id` (string, required) — Opaque Stripe payout identifier (`po_xxx`), unique in Stripe. - `amount_net` (number, required, format: float) — Net amount transferred to the bank account (gross minus fees), in the payout currency. - `fee_total` (number, required, format: float) — Total Stripe fees deducted from the payout. - `amount_gross` (number, required, format: float) — Gross amount before fees. - `currency` (string, required) — ISO-4217 currency code of the payout. - `arrival_date` (string, required, format: date) — Expected arrival date of the payout in the bank account (YYYY-MM-DD). - `status` (string, required, enum: `ingested`, `reconciled`) — Reconciliation state. `ingested` once persisted; `reconciled` (terminal) once matched against a bank statement transaction. - `reconciled_at` (string | null, required, format: date-time) — When the payout was reconciled against the bank statement (ISO 8601), or `null` while `ingested`. - `bank_transaction_ref` (string | null, required, format: uuid) — UUID (v7) of the reconciled bank statement transaction, or `null` while `ingested`. - `composition` (object, required) — Informative breakdown of the payout reported by Stripe (component charges and fees). Component references are opaque Stripe ids (`payment_intent` = `pi_xxx`, `charge_id` = `ch_xxx`), NOT internal payment UUIDs. May be empty when the breakdown could not be read. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/payouts/{payout} — Retrieve a Stripe payout - **Operation ID**: `public-api.v1.payouts.show` - **Tag**: Stripe - **Required scope**: `payouts:read` — Read payouts. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.payouts.show Retrieve a Stripe payout by its `id` (UUID v7). Returns the amounts, currency, arrival date, reconciliation state (`bank_transaction_ref` once reconciled) and the informative `composition` of component charges. Returns 404 if the payout does not exist or belongs to another company. ## Path parameters - `payout` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (StripePayout), required) — A Stripe payout ingested from `payout.paid`, together with its bank-reconciliation state. The public `id` is the payout UUID (v7). `connected_account_id` (`acct_xxx`) and `stripe_payout_id` (`po_xxx`) are external Stripe ids, not foreign keys. `bank_transaction_ref` is the UUID (v7) of the reconciled bank statement transaction (`null` while `status` is `ingested`). `composition` is the informative breakdown reported by Stripe (component charges + fees), referencing opaque Stripe ids — payouts are read-only on the public API; reconciliation happens in the dashboard. - `id` (string, required, format: uuid) — UUID (v7) of the payout. Public identity (KEY `id`). - `object` (string, required, enum: `stripe_payout`) — Stripe-like discriminator. Always `stripe_payout` for this resource. - `connected_account_id` (string, required) — External Stripe connected account id (`acct_xxx`) the payout was paid out from. NOT an internal UUID nor a foreign key. - `stripe_payout_id` (string, required) — Opaque Stripe payout identifier (`po_xxx`), unique in Stripe. - `amount_net` (number, required, format: float) — Net amount transferred to the bank account (gross minus fees), in the payout currency. - `fee_total` (number, required, format: float) — Total Stripe fees deducted from the payout. - `amount_gross` (number, required, format: float) — Gross amount before fees. - `currency` (string, required) — ISO-4217 currency code of the payout. - `arrival_date` (string, required, format: date) — Expected arrival date of the payout in the bank account (YYYY-MM-DD). - `status` (string, required, enum: `ingested`, `reconciled`) — Reconciliation state. `ingested` once persisted; `reconciled` (terminal) once matched against a bank statement transaction. - `reconciled_at` (string | null, required, format: date-time) — When the payout was reconciled against the bank statement (ISO 8601), or `null` while `ingested`. - `bank_transaction_ref` (string | null, required, format: uuid) — UUID (v7) of the reconciled bank statement transaction, or `null` while `ingested`. - `composition` (object, required) — Informative breakdown of the payout reported by Stripe (component charges and fees). Component references are opaque Stripe ids (`payment_intent` = `pi_xxx`, `charge_id` = `ch_xxx`), NOT internal payment UUIDs. May be empty when the breakdown could not be read. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/connected-accounts/{account} — Disconnect a connected Stripe account - **Operation ID**: `public-api.v1.stripe_autoinvoicing.accounts.disconnect` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:write` — Create and update stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.disconnect Disconnect a connected Stripe account without touching the others. The account is marked `disconnected` (its already-issued invoices and history are kept; later webhooks are recorded without processing). Responds 204 with no body. A missing account or one from another company returns 404. ## Path parameters - `account` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/connected-accounts — List connected Stripe accounts - **Operation ID**: `public-api.v1.stripe_autoinvoicing.accounts.list` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:read` — Read stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.list List the connected Stripe accounts (Stripe Connect, multi-store) for your company. Each exposes its `id`, `name`, `external_account_id` (`acct_xxx`), the assigned `series_id`, its per-account configuration (`autoinvoicing_enabled`, `simplified_threshold_cents`, `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`), `status` and `connected_at`. Charges are auto-invoiced with that account's series and configuration. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — UUID (v7) of the connected account. Public identity (KEY `id`). - `object` (string, required, enum: `connected_account`) — Stripe-like discriminator. Always `connected_account` for this resource. - `name` (string, required) — Editable display name of the connected account. - `external_account_id` (string, required) — External Stripe connected account id (`acct_xxx`). NOT an internal UUID nor a foreign key. - `external_account_name` (string | null, required) — Account name reported by Stripe, or `null` if unknown. - `series_id` (string | null, required, format: uuid) — UUID (v7) of the document series used for invoices auto-created from this account. `null` means the company default series is used. - `autoinvoicing_enabled` (boolean, required) — Whether auto-invoicing of charges on this account is enabled. Defaults to `false`. - `simplified_threshold_cents` (integer, required, min 0, max 300000) — Simplified invoice (F2) threshold in cents for this account, range `[0, 300000]` (0–3,000 €). Defaults to 40000 (400 €). - `require_nif` (boolean, required) — Whether a Spanish tax ID (NIF) is required for auto-invoicing on this account. Defaults to `false`. - `refunds_enabled` (boolean, required) — Whether a Stripe refund (`charge.refunded`) on this account automatically generates a linked corrective invoice. Defaults to `true`. - `subscription_autoinvoicing_enabled` (boolean, required) — Whether Stripe subscription cycles on this account are auto-invoiced. Defaults to `false`. - `status` (string, required, enum: `active`, `disconnected`) — Account status. `active` while connected; `disconnected` (terminal) after disconnecting (invoices and history are kept). - `connected_at` (string | null, required, format: date-time) — When the account was connected (ISO 8601), or `null` if unknown. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/connected-accounts/{account} — Retrieve a connected Stripe account - **Operation ID**: `public-api.v1.stripe_autoinvoicing.accounts.show` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:read` — Read stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.show Retrieve a connected Stripe account by its `id` (UUID v7). Returns its name, external account id, assigned series (`series_id`), effective per-account auto-invoicing configuration and status. Returns 404 if the account does not exist or belongs to another company. ## Path parameters - `account` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ConnectedAccount), required) — A connected Stripe account (Stripe Connect, multi-store) for your company. The public `id` is the account UUID (v7). `external_account_id` (`acct_xxx`) is the external Stripe id, not a foreign key. `series_id` is the UUID (v7) of the auto-invoicing document series (`null` means the company default series). The configuration fields are the effective per-account auto-invoicing settings. - `id` (string, required, format: uuid) — UUID (v7) of the connected account. Public identity (KEY `id`). - `object` (string, required, enum: `connected_account`) — Stripe-like discriminator. Always `connected_account` for this resource. - `name` (string, required) — Editable display name of the connected account. - `external_account_id` (string, required) — External Stripe connected account id (`acct_xxx`). NOT an internal UUID nor a foreign key. - `external_account_name` (string | null, required) — Account name reported by Stripe, or `null` if unknown. - `series_id` (string | null, required, format: uuid) — UUID (v7) of the document series used for invoices auto-created from this account. `null` means the company default series is used. - `autoinvoicing_enabled` (boolean, required) — Whether auto-invoicing of charges on this account is enabled. Defaults to `false`. - `simplified_threshold_cents` (integer, required, min 0, max 300000) — Simplified invoice (F2) threshold in cents for this account, range `[0, 300000]` (0–3,000 €). Defaults to 40000 (400 €). - `require_nif` (boolean, required) — Whether a Spanish tax ID (NIF) is required for auto-invoicing on this account. Defaults to `false`. - `refunds_enabled` (boolean, required) — Whether a Stripe refund (`charge.refunded`) on this account automatically generates a linked corrective invoice. Defaults to `true`. - `subscription_autoinvoicing_enabled` (boolean, required) — Whether Stripe subscription cycles on this account are auto-invoiced. Defaults to `false`. - `status` (string, required, enum: `active`, `disconnected`) — Account status. `active` while connected; `disconnected` (terminal) after disconnecting (invoices and history are kept). - `connected_at` (string | null, required, format: date-time) — When the account was connected (ISO 8601), or `null` if unknown. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/connected-accounts/{account} — Update a connected Stripe account - **Operation ID**: `public-api.v1.stripe_autoinvoicing.accounts.update` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:write` — Create and update stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.update Update a connected Stripe account: its `name`, the auto-invoicing `series_id` (`null` clears it, falling back to the company default series) and the per-account fiscal policy (`autoinvoicing_enabled`, `simplified_threshold_cents`, `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`). All fields are optional; omitted ones keep their value. ## Path parameters - `account` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 7 properties; none of them required. Update a connected Stripe account (multi-store). All fields are optional with merge semantics (an omitted field is left unchanged): `name`, `series_id` (auto-invoicing series UUID; `null` clears it back to the company default), `simplified_threshold_cents` (0-300000), `autoinvoicing_enabled`, `require_nif`, `refunds_enabled` and `subscription_autoinvoicing_enabled`. - `name` (string, optional, maxLength 255) - `series_id` (string | null, optional) - `simplified_threshold_cents` (integer, optional, min 0, max 300000) - `autoinvoicing_enabled` (boolean, optional) - `require_nif` (boolean, optional) - `refunds_enabled` (boolean, optional) - `subscription_autoinvoicing_enabled` (boolean, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (ConnectedAccount), required) — A connected Stripe account (Stripe Connect, multi-store) for your company. The public `id` is the account UUID (v7). `external_account_id` (`acct_xxx`) is the external Stripe id, not a foreign key. `series_id` is the UUID (v7) of the auto-invoicing document series (`null` means the company default series). The configuration fields are the effective per-account auto-invoicing settings. - `id` (string, required, format: uuid) — UUID (v7) of the connected account. Public identity (KEY `id`). - `object` (string, required, enum: `connected_account`) — Stripe-like discriminator. Always `connected_account` for this resource. - `name` (string, required) — Editable display name of the connected account. - `external_account_id` (string, required) — External Stripe connected account id (`acct_xxx`). NOT an internal UUID nor a foreign key. - `external_account_name` (string | null, required) — Account name reported by Stripe, or `null` if unknown. - `series_id` (string | null, required, format: uuid) — UUID (v7) of the document series used for invoices auto-created from this account. `null` means the company default series is used. - `autoinvoicing_enabled` (boolean, required) — Whether auto-invoicing of charges on this account is enabled. Defaults to `false`. - `simplified_threshold_cents` (integer, required, min 0, max 300000) — Simplified invoice (F2) threshold in cents for this account, range `[0, 300000]` (0–3,000 €). Defaults to 40000 (400 €). - `require_nif` (boolean, required) — Whether a Spanish tax ID (NIF) is required for auto-invoicing on this account. Defaults to `false`. - `refunds_enabled` (boolean, required) — Whether a Stripe refund (`charge.refunded`) on this account automatically generates a linked corrective invoice. Defaults to `true`. - `subscription_autoinvoicing_enabled` (boolean, required) — Whether Stripe subscription cycles on this account are auto-invoiced. Defaults to `false`. - `status` (string, required, enum: `active`, `disconnected`) — Account status. `active` while connected; `disconnected` (terminal) after disconnecting (invoices and history are kept). - `connected_at` (string | null, required, format: date-time) — When the account was connected (ISO 8601), or `null` if unknown. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/stripe-autoinvoicing/config — Retrieve Stripe autoinvoicing config - **Operation ID**: `public-api.v1.stripe_autoinvoicing.config.show` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:read` — Read stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show Return the Stripe Connect integration state and auto-invoicing configuration: whether Stripe is connected and enabled, the series used, the plan gating, and the fiscal policy (`simplified_threshold_cents`, `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`). With multiple connected accounts it returns 422 `per_account_config_required` — read each via `GET /v1/connected-accounts`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (StripeAutoinvoicingConfig), required) — Stripe Connect integration status and auto-invoicing configuration for your company. Singleton resource (one configuration per company), so it exposes `object` but no navigable `id`. - `object` (string, required, enum: `stripe_autoinvoicing_config`) — Stripe-like discriminator. Always `stripe_autoinvoicing_config` for this resource. - `connected` (boolean, required) — Whether the Stripe Connect integration is connected (active) for the company. - `enabled` (boolean, required) — Whether auto-invoicing of Stripe charges is enabled. Defaults to `false`. - `series_id` (string | null, required, format: uuid) — UUID (v7) of the document series used for auto-created invoices. `null` means the company default series is used. - `available_for_current_plan` (boolean, required) — Whether the current plan includes auto-invoicing (effective plan gating, derived from config — not hardcoded). - `current_plan` (string, required) — The company current plan slug. - `required_plans` (array, required) — Plans that unlock auto-invoicing (derived from config, not hardcoded). - `simplified_threshold_cents` (integer, required, min 0, max 300000) — Simplified invoice (F2) threshold in cents, range `[0, 300000]` (0–3,000 €). Charges without a tax ID at or below this amount are auto-issued as simplified invoices; above it they are routed to manual review. Defaults to 40000 (400 €). - `require_nif` (boolean, required) — Whether a Spanish tax ID (NIF) is required for auto-invoicing. When enabled, Factuarea-created Checkouts mark the tax ID as required and charges without a tax ID are never auto-issued as simplified invoices (they go to manual review). Defaults to `false`. - `refunds_enabled` (boolean, required) — Whether a Stripe refund (`charge.refunded`) automatically generates a linked corrective invoice (with the original invoice and the VeriFactu R record). Only acts while auto-invoicing is enabled. Defaults to `true`. - `subscription_autoinvoicing_enabled` (boolean, required) — Whether Stripe subscription cycles (`invoice.paid` with a standard `billing_reason`) are auto-invoiced. Both this toggle and the general `enabled` flag must be on for a cycle to be invoiced. Defaults to `false`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/stripe-autoinvoicing/config — Update Stripe autoinvoicing config - **Operation ID**: `public-api.v1.stripe_autoinvoicing.config.update` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:write` — Create and update stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.update Enable or disable auto-invoicing of Stripe Connect charges and choose the series used. Optionally tune the fiscal policy (`simplified_threshold_cents` in cents [0, 300000], `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`); omitted fields keep their value. With multiple connected accounts it returns 422 `per_account_config_required` — configure each account individually. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 1 required: `enabled`. Update the Stripe auto-invoicing configuration. `enabled` toggles auto-invoicing; `series_id` (UUID, nullable) sets the series for auto-created invoices (`null` uses the company default); `simplified_threshold_cents` (0-300000), `require_nif`, `refunds_enabled` and `subscription_autoinvoicing_enabled` are optional partial fields. - `enabled` (boolean, required) - `series_id` (string | null, optional) - `simplified_threshold_cents` (integer, optional, min 0, max 300000) - `require_nif` (boolean, optional) - `refunds_enabled` (boolean, optional) - `subscription_autoinvoicing_enabled` (boolean, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (StripeAutoinvoicingConfig), required) — Stripe Connect integration status and auto-invoicing configuration for your company. Singleton resource (one configuration per company), so it exposes `object` but no navigable `id`. - `object` (string, required, enum: `stripe_autoinvoicing_config`) — Stripe-like discriminator. Always `stripe_autoinvoicing_config` for this resource. - `connected` (boolean, required) — Whether the Stripe Connect integration is connected (active) for the company. - `enabled` (boolean, required) — Whether auto-invoicing of Stripe charges is enabled. Defaults to `false`. - `series_id` (string | null, required, format: uuid) — UUID (v7) of the document series used for auto-created invoices. `null` means the company default series is used. - `available_for_current_plan` (boolean, required) — Whether the current plan includes auto-invoicing (effective plan gating, derived from config — not hardcoded). - `current_plan` (string, required) — The company current plan slug. - `required_plans` (array, required) — Plans that unlock auto-invoicing (derived from config, not hardcoded). - `simplified_threshold_cents` (integer, required, min 0, max 300000) — Simplified invoice (F2) threshold in cents, range `[0, 300000]` (0–3,000 €). Charges without a tax ID at or below this amount are auto-issued as simplified invoices; above it they are routed to manual review. Defaults to 40000 (400 €). - `require_nif` (boolean, required) — Whether a Spanish tax ID (NIF) is required for auto-invoicing. When enabled, Factuarea-created Checkouts mark the tax ID as required and charges without a tax ID are never auto-issued as simplified invoices (they go to manual review). Defaults to `false`. - `refunds_enabled` (boolean, required) — Whether a Stripe refund (`charge.refunded`) automatically generates a linked corrective invoice (with the original invoice and the VeriFactu R record). Only acts while auto-invoicing is enabled. Defaults to `true`. - `subscription_autoinvoicing_enabled` (boolean, required) — Whether Stripe subscription cycles (`invoice.paid` with a standard `billing_reason`) are auto-invoiced. Both this toggle and the general `enabled` flag must be on for a cycle to be invoiced. Defaults to `false`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/stripe-autoinvoicing/correctives — List Stripe autoinvoiced correctives - **Operation ID**: `public-api.v1.stripe_autoinvoicing.correctives.list` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:read` — Read stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list List the corrective invoices automatically generated from Stripe refunds (`charge.refunded`), with cursor-based pagination. The public `id` is the corrective invoice (UUID v7); `original_invoice_id` links to the original invoice, and `refund_id` is the originating gateway refund. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — UUID (v7) of the corrective invoice. Public identity (KEY `id`). - `object` (string, required, enum: `stripe_autoinvoiced_corrective`) — Stripe-like discriminator. Always `stripe_autoinvoiced_corrective` for this resource. - `original_invoice_id` (string, required, format: uuid) — UUID (v7) of the original invoice that was corrected by the refund. - `refund_id` (string, required) — Opaque refund identifier from the payment gateway (e.g. Stripe `re_xxx`). - `provider` (string, required) — Payment gateway the refund originated from. - `amount` (number, required, format: float) — Refunded amount in euros (the individual refund amount, not the cumulative). - `correction_type` (string, required, enum: `total`, `partial`) — Scope of the correction: `total` (full annulment) or `partial` (a single negative line for the refunded amount). - `created_at` (string, required, format: date-time) — When the corrective mapping was recorded (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/stripe-autoinvoicing/payments — List Stripe autoinvoiced charges - **Operation ID**: `public-api.v1.stripe_autoinvoicing.payments.list` - **Tag**: Stripe - **Required scope**: `stripe_autoinvoicing:read` — Read stripe autoinvoicing. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list List the Stripe charges that generated an invoice (flows A and B plus subscription cycles), with cursor-based pagination. The generated invoice and client are returned as `invoice_id`/`client_id`. Subscription-cycle charges also expose `subscription_id` (external `sub_xxx`), `stripe_invoice_id` and the billed period. Filter by `origin` (`subscription`/`oneshot`). ## Query parameters - `origin` (string, optional, enum: `subscription`, `oneshot`) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — UUID (v7) of the payment record. Public identity (KEY `id`). - `object` (string, required, enum: `stripe_autoinvoiced_payment`) — Stripe-like discriminator. Always `stripe_autoinvoiced_payment` for this resource. - `invoice_id` (string, required, format: uuid) — UUID (v7) of the invoice generated by this charge. - `client_id` (string | null, required, format: uuid) — UUID (v7) of the invoice client, or `null` if not available. - `invoice_number` (string | null, required) — Number of the generated invoice. - `client_name` (string | null, required) — Denormalized client name for display. - `amount` (number, required, format: float) — Charged amount in euros. - `payment_date` (string, required, format: date) — Date of the charge (YYYY-MM-DD). - `stripe_reference` (string | null, required) — Stripe transaction reference of the payment record. - `subscription_id` (string | null, required) — External Stripe subscription id (`sub_xxx`) when the charge comes from a subscription cycle; `null` for one-shot charges. This is an external Stripe id, NOT an internal UUID. - `stripe_invoice_id` (string | null, required) — Stripe invoice id (`in_xxx`) of the billed cycle; `null` for one-shot charges. - `period_start` (string | null, required, format: date) — Start of the billed subscription period (YYYY-MM-DD); `null` for one-shot charges. - `period_end` (string | null, required, format: date) — End of the billed subscription period (YYYY-MM-DD); `null` for one-shot charges. - `created_at` (string, required, format: date-time) — When the payment record was created (ISO 8601). - `updated_at` (string, required, format: date-time) — When the payment record was last updated (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/suppliers/{supplier}/activities — List supplier activity timeline - **Operation ID**: `public-api.v1.suppliers.activities` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.activities Return the audit timeline for a supplier combining its own domain events plus purchase invoice and contract events that reference it. Paginated with page and per_page query params (default 50). ## Path parameters - `supplier` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `supplier.updated`, `purchase_invoice.created`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/suppliers/bulk-delete — Delete multiple suppliers in bulk - **Operation ID**: `public-api.v1.suppliers.bulk_delete` - **Tag**: Suppliers - **Required scope**: `suppliers:delete` — Delete suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.bulk_delete Delete up to 200 suppliers in one request. Returns a `BulkPartialSuccessResult` with `total`, `successful` and `failed` counts plus a `failures` list (`id` + `error_code` + Spanish `error_message`); suppliers with associated contracts are reported in `failures`. UUIDs from other tenants are ignored. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `ids`. Delete several suppliers in one request. `ids` is an array of 1 to 200 UUIDs; identifiers that do not belong to your company are reported under `failed`. - `ids` (array, required, maxItems 200) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/suppliers/bulk-status — Bulk change supplier active state - **Operation ID**: `public-api.v1.suppliers.bulk_status` - **Tag**: Suppliers - **Required scope**: `suppliers:write` — Create and update suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.bulk_status Move up to 50 suppliers (by id) to the target `new_status` (`active` or `inactive`). Idempotent with respect to the target: a supplier already in the requested state counts as `successful` without flipping. Returns a `BulkPartialSuccessResult`; suppliers not found come back in `failures[]`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `new_status`, `ids`. Transition several suppliers to `new_status` (`active` or `inactive`) in one request, up to 50 per batch. `ids` is an array of supplier UUIDs; the change is idempotent (a supplier already in the target status counts as successful). Suppliers that do not exist are returned under `failures[]`. - `new_status` (string, required, enum: `active`, `inactive`) - `ids` (array, required, maxItems 50) ## Responses - **200** - Body (`application/json`): - `data` (object (BulkPartialSuccessResult), required) — Result of a bulk or import operation that reports per-resource status. `total` is how many rows/resources were processed (`successful + failed`), `successful` how many were applied (deleted, created or validated) and `failed` how many could not be processed. `failures[]` carries one item per failed row. Shape shared by every bulk endpoint of the public API (the `/v1/{resource}/bulk-delete` endpoints emit it today). Anchored integrators before `2026-09-01` keep receiving the previous `{object, deleted, failed[{id, reason}]}` shape via `Factuarea-Version`. - `total` (integer, required) — Number of rows/resources processed (`successful + failed`). - `successful` (integer, required) — Number of rows/resources processed successfully (deleted, created or validated). - `failed` (integer, required) — Number of rows/resources that could not be processed. Equals `failures` length. - `failures` (array, required) — One item per failed row/resource. Always a list (empty, never `null`, when there are no failures). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/suppliers — Create a supplier - **Operation ID**: `public-api.v1.suppliers.create` - **Tag**: Suppliers - **Required scope**: `suppliers:write` — Create and update suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.create Create a new supplier (vendor) for your company. The returned object includes the generated `uuid`. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 31 properties; 1 required: `name`. - `name` (string, required, maxLength 200) - `business_name` (string | null, optional, maxLength 200) - `commercial_name` (string | null, optional, maxLength 200) - `tax_id` (string | null, optional, maxLength 50) - `vat_id` (string | null, optional, maxLength 20) - `email` (string | null, optional, format: email, maxLength 191) - `phone` (string | null, optional, maxLength 20, pattern: `^\+?[0-9\s\-()]{6,20}$`) - `fax` (string | null, optional, maxLength 20) - `mobile` (string | null, optional, maxLength 20) - `website` (string | null, optional, format: uri, maxLength 255) - `contact_person` (string | null, optional, maxLength 200) - `latitude` (number | null, optional, min -90, max 90) - `longitude` (number | null, optional, min -180, max 180) - `default_discount` (number | null, optional, min 0, max 100) - `default_vat_rate` (number | null, optional, min 0, max 100) - `default_retention_rate` (number | null, optional, min 0, max 100) - `is_surcharge_subject` (boolean | null, optional) - `accumulate_347` (boolean, optional) - `iban` (string | null, optional, maxLength 34, pattern: `^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9 ]{11,42}$`) - `default_taxes_id` (string | null, optional, format: uuid) - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`) - `payment_method` (string | null, optional, enum: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`) - `payment_terms_days` (integer | null, optional, min 0, max 365) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) - `billing_emails` (array | null, optional, maxItems 5) - `coordinates` (object, optional) - `latitude` (number | null, optional, min -90, max 90) - `longitude` (number | null, optional, min -180, max 180) - `alternative_id` (object, optional) - `type` (string, optional, enum: `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`, `tax_id_foreign`, `national_id`) — Alternative identifier type from the AEAT L7 catalog. Legacy aliases (`tax_id_foreign`/`national_id`) are accepted on input for backward compatibility. - `value` (string, optional, maxLength 50, minLength 1) - `country_code` (string, optional, maxLength 2, minLength 2, pattern: `^[A-Z]{2}$`) - `address` (object, optional) - `line1` (string | null, optional, maxLength 500) - `line2` (string | null, optional, maxLength 100) - `number` (string | null, optional, maxLength 100) - `floor` (string | null, optional, maxLength 100) - `door` (string | null, optional, maxLength 100) - `staircase` (string | null, optional, maxLength 100) - `postal_code` (string | null, optional, maxLength 10) - `city` (string | null, optional, maxLength 100) - `province` (string | null, optional, maxLength 100) - `country` (string | null, optional, maxLength 2, minLength 2) - `bank_accounts` (array | null, optional) - `iban` (string, required, maxLength 50, pattern: `^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9 ]{11,42}$`) - `bic` (string | null, optional, maxLength 20, pattern: `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$`) - `is_default` (boolean | null, optional) - `notes` (string | null, optional, maxLength 255) ## Responses - **201** — Supplier created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Supplier), required) — A supplier or vendor of your company. - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/suppliers/{supplier} — Delete a supplier - **Operation ID**: `public-api.v1.suppliers.delete` - **Tag**: Suppliers - **Required scope**: `suppliers:delete` — Delete suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.delete Delete a supplier. Returns 422 if the supplier is referenced by any purchase invoice. ## Path parameters - `supplier` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/suppliers/find-by-external-id — Find a supplier by external ID - **Operation ID**: `public-api.v1.suppliers.find_by_external_id` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.find_by_external_id Look up a supplier by its `external_id` (sent in the JSON body), the persistent integration key that maps it to a record in a third-party system (ERP/CRM). Distinct from the fiscal `tax_id` and from the request-level `Idempotency-Key`. Returns the matching supplier or 404 if no supplier uses that external_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `external_id`. Look up a supplier by its `external_id` (the integration key that maps it to a record in a third-party ERP/CRM/e-commerce system) within your company. - `external_id` (string, required, maxLength 100) ## Responses - **200** - Body (`application/json`): - `data` (object (Supplier), required) — A supplier or vendor of your company. - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/suppliers/find-by-tax-id — Find a supplier by tax ID - **Operation ID**: `public-api.v1.suppliers.find_by_tax_id` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.find_by_tax_id Look up a supplier by its Spanish tax identifier (NIF/CIF/NIE/VAT). Returns the matching supplier or 404 if no supplier uses that tax_id within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `tax_id`. Look up a supplier by its Spanish tax ID (NIF/CIF/NIE/VAT) within your company. - `tax_id` (string, required, maxLength 50) ## Responses - **200** - Body (`application/json`): - `data` (object (Supplier), required) — A supplier or vendor of your company. - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/suppliers — List all suppliers - **Operation ID**: `public-api.v1.suppliers.list` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.list List your suppliers with cursor-based pagination. Supports filtering by `is_active`, `created_at[gte|lte]`. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `tax_id` (string, optional) — Fiscal tax number (NIF/CIF/NIE) of the supplier. - `tax_id[in]` (string, optional) — Fiscal tax number (NIF/CIF/NIE) of the supplier. - `tax_id[contains]` (string, optional) — Fiscal tax number (NIF/CIF/NIE) of the supplier. - `vat_id` (string, optional) — Intra-community VAT number. - `vat_id[contains]` (string, optional) — Intra-community VAT number. - `name` (string, optional) — Trade name of the supplier. - `name[contains]` (string, optional) — Trade name of the supplier. - `city` (string, optional) — City of the supplier postal address. - `city[contains]` (string, optional) — City of the supplier postal address. - `province` (string, optional) — Province / region of the supplier postal address. - `province[contains]` (string, optional) — Province / region of the supplier postal address. - `is_active` (boolean, optional) — Filter by active / inactive suppliers. - `created[gte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Creation date (ISO 8601). - `search` (string, optional, maxLength 80) — Free-text search. - `metadata` (object, optional) — Filter by metadata key/value pairs using the deepObject syntax `metadata[key]=value`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/suppliers/search — Search suppliers - **Operation ID**: `public-api.v1.suppliers.search` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.search Search suppliers by free-text query against `name`, `tax_id`, `vat_id`, `email`, and `phone`. Capped at 50 results. ## Query parameters - `q` (string, required, maxLength 120, minLength 1) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/suppliers/{supplier} — Retrieve a supplier - **Operation ID**: `public-api.v1.suppliers.show` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.show Retrieve a supplier by its `uuid`. ## Path parameters - `supplier` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Supplier), required) — A supplier or vendor of your company. - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/suppliers/stats — Get supplier stats - **Operation ID**: `public-api.v1.suppliers.stats` - **Tag**: Suppliers - **Required scope**: `suppliers:read` — Read suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.stats Aggregated KPIs for the authenticated company: total supplier count, active count, count with contracts, and amount totals by status. Returned as `{ "data": SupplierStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (SupplierStats), required) — Resumen agregado de la cartera de proveedores de la empresa autenticada: contadores y proveedores con facturas de compra. Devuelto por `GET /v1/suppliers/stats`. - `object` (string, required, enum: `supplier_stats`) - `total` (integer, required) — Total number of suppliers registered in the company. - `active` (integer, required) — Proveedores marcados como activos. - `inactive` (integer, required) — Proveedores marcados como inactivos. - `with_purchase_invoices` (integer, required) — Suppliers with at least one recorded purchase invoice. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/suppliers/{supplier}/toggle-active — Toggle supplier active state - **Operation ID**: `public-api.v1.suppliers.toggle_active` - **Tag**: Suppliers - **Required scope**: `suppliers:write` — Create and update suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.toggle_active Flip a supplier between active and inactive. Inactive suppliers are hidden from line-item selectors on new purchase invoices. ## Path parameters - `supplier` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Supplier), required) — A supplier or vendor of your company. - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/suppliers/{supplier} — Update a supplier - **Operation ID**: `public-api.v1.suppliers.update` - **Tag**: Suppliers - **Required scope**: `suppliers:write` — Create and update suppliers. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/suppliers/public-api.v1.suppliers.update Update a supplier. Only fields present in the payload are modified. ## Path parameters - `supplier` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 32 properties; none of them required. Public REST API v1 — PUT /v1/suppliers/{uuid}. Partial PUT update: all fields are `sometimes`. If not sent, the handler keeps the current value. If sent as `null`, the field is cleared (when the domain allows it). Accepts the same fields as `CreateSupplierRequest` V1 (see docblock there). The validation of domain invariants (XOR `tax_id`/`alternative_id`, direct_debit ⇒ default bank account, billing_emails without duplicates, IBAN format, metadata limits) is performed by the `Supplier` aggregate and the VOs. The typed exceptions propagate to the `ExceptionRenderer` with the canonical v1 envelope. - `name` (string, optional, maxLength 200) - `business_name` (string | null, optional, maxLength 200) - `commercial_name` (string | null, optional, maxLength 200) - `tax_id` (string | null, optional, maxLength 50) - `vat_id` (string | null, optional, maxLength 20) - `email` (string | null, optional, format: email, maxLength 191) - `phone` (string | null, optional, maxLength 20, pattern: `^\+?[0-9\s\-()]{6,20}$`) - `fax` (string | null, optional, maxLength 20) - `mobile` (string | null, optional, maxLength 20) - `website` (string | null, optional, format: uri, maxLength 255) - `contact_person` (string | null, optional, maxLength 200) - `latitude` (number | null, optional, min -90, max 90) - `longitude` (number | null, optional, min -180, max 180) - `default_discount` (number | null, optional, min 0, max 100) - `default_vat_rate` (number | null, optional, min 0, max 100) - `default_retention_rate` (number | null, optional, min 0, max 100) - `is_surcharge_subject` (boolean | null, optional) - `accumulate_347` (boolean, optional) - `is_active` (boolean, optional) - `iban` (string | null, optional, maxLength 34, pattern: `^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9 ]{11,42}$`) - `default_taxes_id` (string | null, optional, format: uuid) - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`) - `payment_method` (string | null, optional, enum: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`) - `payment_terms_days` (integer | null, optional, min 0, max 365) - `notes` (string | null, optional, maxLength 1000) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `external_id` (string | null, optional, maxLength 100) - `billing_emails` (array | null, optional, maxItems 5) - `coordinates` (object, optional) - `latitude` (number | null, optional, min -90, max 90) - `longitude` (number | null, optional, min -180, max 180) - `alternative_id` (object, optional) - `type` (string, optional, enum: `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`, `tax_id_foreign`, `national_id`) — Alternative identifier type from the AEAT L7 catalog. Legacy aliases (`tax_id_foreign`/`national_id`) are accepted on input for backward compatibility. - `value` (string, optional, maxLength 50, minLength 1) - `country_code` (string, optional, maxLength 2, minLength 2, pattern: `^[A-Z]{2}$`) - `address` (object, optional) - `line1` (string | null, optional, maxLength 500) - `line2` (string | null, optional, maxLength 100) - `number` (string | null, optional, maxLength 100) - `floor` (string | null, optional, maxLength 100) - `door` (string | null, optional, maxLength 100) - `staircase` (string | null, optional, maxLength 100) - `postal_code` (string | null, optional, maxLength 10) - `city` (string | null, optional, maxLength 100) - `province` (string | null, optional, maxLength 100) - `country` (string | null, optional, maxLength 2, minLength 2) - `bank_accounts` (array | null, optional) - `iban` (string, required, maxLength 50, pattern: `^[A-Za-z]{2}[0-9]{2}[A-Za-z0-9 ]{11,42}$`) - `bic` (string | null, optional, maxLength 20, pattern: `^[A-Za-z]{6}[A-Za-z0-9]{2}([A-Za-z0-9]{3})?$`) - `is_default` (boolean | null, optional) - `notes` (string | null, optional, maxLength 255) ## Responses - **200** - Body (`application/json`): - `data` (object (Supplier), required) — A supplier or vendor of your company. - `id` (string, required) - `object` (string, required, enum: `supplier`) - `name` (string, required) - `business_name` (string | null, optional) — Legal/business name / full fiscal name of the supplier (may match `name`). - `commercial_name` (string | null, optional) — Optional trade name (DBA), distinct from the legal name. - `tax_id` (string | null, required, pattern: `^(\d{8}[A-Z]|[XYZ]\d{7}[A-Z]|[A-Z]\d{7}[A-Z0-9])$`) — Spanish fiscal identifier (NIF, CIF, NIE). Structural format: NIF `^\d{8}[A-Z]$`, NIE `^[XYZ]\d{7}[A-Z]$`, CIF `^[A-Z]\d{7}[A-Z0-9]$`. AEAT control-digit (checksum) validation is enforced when the request opts in via the `Factuarea-Version` header on or after the activation version; without that opt-in the legacy permissive behaviour is preserved. Mutually exclusive with `alternative_id`. - `vat_id` (string | null, required) — EU VAT identifier. - `email` (string | null, required, format: email) - `phone` (string | null, required) - `fax` (string | null, optional) — Fax number (rarely used, legacy). - `mobile` (string | null, optional) — Mobile number. - `website` (string | null, optional, format: uri) — Supplier website. - `contact_person` (string | null, optional, maxLength 200) — B2B contact person. - `billing_emails` (array, optional, maxItems 5) — Additional emails for invoice delivery (administration, accounting). Maximum 5. - `address` (object (Address), required) - `coordinates` (object | null, optional) — Geographic coordinates of the supplier. `null` when not recorded. - `iban` (string | null, required) — Legacy alias for bank_accounts[].iban where is_default=true. Kept for backward compatibility with existing integrators; prefer reading `bank_accounts[]` for new consumers. - `bank_accounts` (array, optional) — Bank accounts associated with the supplier. Empty `[]` when there are none. - `default_taxes_id` (string | null, optional) — UUID (v7) of the default tax applied to the supplier. - `default_discount` (number | null, optional, format: float) — Default discount applied to the supplier (percentage). - `default_vat_rate` (number | null, optional, format: float) — Default VAT rate applied to the supplier (percentage). - `default_retention_rate` (number | null, optional, format: float) — Default IRPF withholding rate (percentage). - `is_surcharge_subject` (boolean, optional) — Indica si al proveedor se le aplica recargo de equivalencia. - `preferred_operation_regime` (string | null, optional, enum: `general`, `intracomunitaria`, `importacion_exportacion`, `isp`, `null`) — Preferred operation regime of the supplier for VAT / VeriFactu purposes. - `accumulate_347` (boolean, required) — Whether this supplier accumulates towards the annual Modelo 347 report (operations with third parties above the legal threshold). - `alternative_id` (object (AlternativeId) | null, optional) - `payment_preferences` (object (PaymentPreferences) | null, optional) - `external_id` (string | null, optional, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this supplier to a record in a third-party system. Free-format, unique per company, distinct from the fiscal `tax_id`. Persistent ERP synchronization key, independent of the request-level `Idempotency-Key`. - `notes` (string | null, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `is_active` (boolean, required) - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/tax_reports/{tax_report}/activities — List tax report activities - **Operation ID**: `public-api.v1.tax_reports.activities` - **Tag**: Tax Reports - **Required scope**: `tax_reports:read` — Read tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.activities Returns the cursor-paginated activity timeline (generation, download, etc.) of a single tax report generation. ## Path parameters - `tax_report` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `tax_report.generated`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs) are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/tax_reports/{tax_report}/download — Download tax report file - **Operation ID**: `public-api.v1.tax_reports.download` - **Tag**: Tax Reports - **Required scope**: `tax_reports:read` — Read tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.download Downloads the generated file for a tax report. Adds `X-Tax-Report-Hash` header for integrity verification. ## Path parameters - `tax_report` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - string | null - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/tax_reports/find-by-period — Find a tax report by period - **Operation ID**: `public-api.v1.tax_reports.find_by_period` - **Tag**: Tax Reports - **Required scope**: `tax_reports:read` — Read tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.find_by_period Looks up the most recent generated tax report for a given type and period. Returns the report or 404 `tax_report_not_found` when none exists for the period. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `type`, `year`. Resolve the most recent generation of a tax report for a period (latest wins). Validates the shape (`year` integer, `quarter` 1-4, `type`); the period rules (year range, and whether the model requires or forbids a quarter) are enforced with a 422. `303`/`347` are accepted as aliases of `modelo_303`/`modelo_347`. - `type` (string, required, enum: `modelo_303`, `modelo_347`, `modelo_130`) - `year` (integer, required) - `quarter` (integer | null, optional, min 1, max 4) ## Responses - **200** - Body (`application/json`): - `data` (object (TaxReport), required) — A generated Spanish tax declaration (Modelo 303 quarterly VAT, or Modelo 347 yearly informational). Downloadable via `GET /v1/tax_reports/{uuid}/download`. - `id` (string, required) — Opaque UUID (v7) of the generated report. - `object` (string, required, enum: `tax_report`) - `type` (string, required, enum: `modelo_303`, `modelo_347`) — Declaration type. - `period_year` (integer, required) — Fiscal year covered by the declaration (backward compat, also available in `period.year`). - `period_quarter` (integer | null, required, min 1, max 4) — Quarter (1-4) for Modelo 303; null for Modelo 347 (yearly). Backward compat, also available in `period.quarter`. - `period` (object, required) — Fiscal period covered by the declaration (nested form). - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) — Output format. `txt_aeat` is the official format accepted by the AEAT submission portal. - `hash` (string, required) — SHA-256 of the generated file (audit trail). - `size_bytes` (integer, required) - `generated_at` (string, required, format: date-time) - `generated_by_id` (string, required) — UUID (v7) of the user who generated the declaration (audit trail). - `download_url` (string, required) — Relative download URL of the generated resource (`/v1/tax_reports/{uuid}/download`). - `warnings` (array, required) — Non-blocking warnings emitted during generation (e.g. invoices with incomplete data). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/tax_reports/130 — Generate Modelo 130 - **Operation ID**: `public-api.v1.tax_reports.generate_130` - **Tag**: Tax Reports - **Required scope**: `tax_reports:write` — Create and update tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.generate_130 Generates the Spanish Modelo 130 (quarterly IRPF instalment payment, direct estimation) for the given year and quarter in the requested format (txt_aeat, pdf, excel; defaults to pdf). The calculation is cumulative year-to-date (1 Jan to end of quarter). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 2 required: `year`, `format`. Generate Modelo 130 (IRPF instalment payment, direct estimation). `format` defaults to `pdf` when omitted; allowed values are `txt_aeat`, `pdf` and `excel` (only `txt_aeat` is directly submittable on the AEAT portal). `year` and `quarter` are validated for range and the model period rules, returning 422 on a violation. - `year` (integer, required) - `quarter` (integer | null, optional) - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) - `deduccion_vivienda_centimos` (integer | null, optional, min 0) — Optional period inputs (in cents) for boxes [16] / [18] / override [05]. - `resultado_complementaria_centimos` (integer | null, optional, min 0) - `pagos_fraccionados_anteriores_override_centimos` (integer | null, optional, min 0) ## Responses - **201** - Body (`application/json`): - `data` (array, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/tax_reports/303 — Generate Modelo 303 - **Operation ID**: `public-api.v1.tax_reports.generate_303` - **Tag**: Tax Reports - **Required scope**: `tax_reports:write` — Create and update tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.generate_303 Generates the Spanish Modelo 303 (quarterly VAT) for the given year and quarter in the requested format (txt_aeat, pdf, excel). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `year`, `format`. Generate Modelo 303 (VAT self-assessment). `format` defaults to `pdf` when omitted; allowed values are `txt_aeat`, `pdf` and `excel` (only `txt_aeat` is directly submittable on the AEAT portal). `year` and `quarter` are validated for range and the 303 period rules, returning 422 on a violation. - `year` (integer, required) - `quarter` (integer | null, optional) - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) ## Responses - **201** — Modelo 303 generated successfully. The `Location` header contains the relative download URL of the generated resource. - Body (`application/json`): - `data` (object (TaxReport), required) — A generated Spanish tax declaration (Modelo 303 quarterly VAT, or Modelo 347 yearly informational). Downloadable via `GET /v1/tax_reports/{uuid}/download`. - `id` (string, required) — Opaque UUID (v7) of the generated report. - `object` (string, required, enum: `tax_report`) - `type` (string, required, enum: `modelo_303`, `modelo_347`) — Declaration type. - `period_year` (integer, required) — Fiscal year covered by the declaration (backward compat, also available in `period.year`). - `period_quarter` (integer | null, required, min 1, max 4) — Quarter (1-4) for Modelo 303; null for Modelo 347 (yearly). Backward compat, also available in `period.quarter`. - `period` (object, required) — Fiscal period covered by the declaration (nested form). - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) — Output format. `txt_aeat` is the official format accepted by the AEAT submission portal. - `hash` (string, required) — SHA-256 of the generated file (audit trail). - `size_bytes` (integer, required) - `generated_at` (string, required, format: date-time) - `generated_by_id` (string, required) — UUID (v7) of the user who generated the declaration (audit trail). - `download_url` (string, required) — Relative download URL of the generated resource (`/v1/tax_reports/{uuid}/download`). - `warnings` (array, required) — Non-blocking warnings emitted during generation (e.g. invoices with incomplete data). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/tax_reports/347 — Generate Modelo 347 - **Operation ID**: `public-api.v1.tax_reports.generate_347` - **Tag**: Tax Reports - **Required scope**: `tax_reports:write` — Create and update tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.generate_347 Generates the Spanish Modelo 347 (annual third-party operations > 3,005.06 EUR) for the given year. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `year`, `format`. Generate Modelo 347 (annual declaration of third-party operations). `format` defaults to `pdf` when omitted; allowed values are `txt_aeat`, `pdf` and `excel`. Modelo 347 is annual, so sending a `quarter` returns 422. - `year` (integer, required) - `quarter` (integer | null, optional) - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) ## Responses - **201** — Modelo 347 generated successfully. The `Location` header contains the relative download URL of the generated resource. - Body (`application/json`): - `data` (object (TaxReport), required) — A generated Spanish tax declaration (Modelo 303 quarterly VAT, or Modelo 347 yearly informational). Downloadable via `GET /v1/tax_reports/{uuid}/download`. - `id` (string, required) — Opaque UUID (v7) of the generated report. - `object` (string, required, enum: `tax_report`) - `type` (string, required, enum: `modelo_303`, `modelo_347`) — Declaration type. - `period_year` (integer, required) — Fiscal year covered by the declaration (backward compat, also available in `period.year`). - `period_quarter` (integer | null, required, min 1, max 4) — Quarter (1-4) for Modelo 303; null for Modelo 347 (yearly). Backward compat, also available in `period.quarter`. - `period` (object, required) — Fiscal period covered by the declaration (nested form). - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) — Output format. `txt_aeat` is the official format accepted by the AEAT submission portal. - `hash` (string, required) — SHA-256 of the generated file (audit trail). - `size_bytes` (integer, required) - `generated_at` (string, required, format: date-time) - `generated_by_id` (string, required) — UUID (v7) of the user who generated the declaration (audit trail). - `download_url` (string, required) — Relative download URL of the generated resource (`/v1/tax_reports/{uuid}/download`). - `warnings` (array, required) — Non-blocking warnings emitted during generation (e.g. invoices with incomplete data). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/tax_reports/history — List tax report history - **Operation ID**: `public-api.v1.tax_reports.history` - **Tag**: Tax Reports - **Required scope**: `tax_reports:read` — Read tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.history Returns the paginated history of generated tax reports for the company. Optional filters: type, year. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) — Opaque UUID (v7) of the generated report. - `object` (string, required, enum: `tax_report`) - `type` (string, required, enum: `modelo_303`, `modelo_347`) — Declaration type. - `period_year` (integer, required) — Fiscal year covered by the declaration (backward compat, also available in `period.year`). - `period_quarter` (integer | null, required, min 1, max 4) — Quarter (1-4) for Modelo 303; null for Modelo 347 (yearly). Backward compat, also available in `period.quarter`. - `period` (object, required) — Fiscal period covered by the declaration (nested form). - `format` (string, required, enum: `txt_aeat`, `pdf`, `excel`) — Output format. `txt_aeat` is the official format accepted by the AEAT submission portal. - `hash` (string, required) — SHA-256 of the generated file (audit trail). - `size_bytes` (integer, required) - `generated_at` (string, required, format: date-time) - `generated_by_id` (string, required) — UUID (v7) of the user who generated the declaration (audit trail). - `download_url` (string, required) — Relative download URL of the generated resource (`/v1/tax_reports/{uuid}/download`). - `warnings` (array, required) — Non-blocking warnings emitted during generation (e.g. invoices with incomplete data). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/tax_reports/preview — Preview a tax report - **Operation ID**: `public-api.v1.tax_reports.preview` - **Tag**: Tax Reports - **Required scope**: `tax_reports:read` — Read tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.preview Computes the breakdown of a tax report without persisting a generation or writing files. Ideal for interactive UIs that confirm totals before commit. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `type`, `year`. Public REST API v1 — POST /v1/tax_reports/preview. Computes the report breakdown without persisting any generation or file. Useful for showing the user what they are about to declare before confirming. We accept `303` / `347` as aliases of the canonical values `modelo_303` / `modelo_347`, keeping consistency with the paths `POST /v1/tax_reports/303` and `POST /v1/tax_reports/347`. The handler normalizes the value before instantiating the VO `TaxReportType`. - `type` (string, required, enum: `modelo_303`, `modelo_347`, `modelo_130`) - `year` (integer, required, min 2024, max 2027) - `quarter` (integer | null, optional, min 1, max 4) ## Responses - **200** - Body (`application/json`): - `data` (object (TaxReportPreview), required) — Preview (dry-run computation, without persisting any file) of a Modelo 303/347 for a period. Lets the integrator validate the amounts before generating the final report. Amounts are expressed in euro cents (integers). - `object` (string, required, enum: `tax_report_preview`) - `type` (string, required, enum: `modelo_303`, `modelo_347`) — Type of previewed report. - `period` (object, required) — Previewed fiscal period. - `breakdown_303` (object, required) — Breakdown by VAT rate (Modelo 303 only). Map indexed by tax rate → `{base, cuota}` in cents. Empty for Modelo 347. - `totals` (object, required) — Aggregated totals of the declaration (amounts in cents). - `clients_347` (array, required) — Operations with clients that exceed the Modelo 347 threshold (Modelo 347 only). Empty for Modelo 303. - `suppliers_347` (array, required) — Operations with suppliers that exceed the Modelo 347 threshold (Modelo 347 only). Empty for Modelo 303. - `invoice_count` (integer, required) — Number of issued invoices considered in the period. - `purchase_invoice_count` (integer, required) — Number of purchase invoices considered in the period. - `warnings` (array, required) — Non-blocking warnings detected during the computation (e.g. invoices with incomplete data). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed for the tax report request — e.g. an invalid period (Modelo 303 requires a quarter, Modelo 347 does not admit one, or the year is out of range), no invoices in the period, or an unsupported output format. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/tax_reports/stats — Retrieve tax report stats - **Operation ID**: `public-api.v1.tax_reports.stats` - **Tag**: Tax Reports - **Required scope**: `tax_reports:read` — Read tax reports. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/tax-reports/public-api.v1.tax_reports.stats Returns aggregate KPIs of the generated tax report history: totals by type and format, accumulated file size, and the current fiscal quarter/year. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TaxReportStats), required) — Aggregated summary of the history of tax reports generated by the authenticated company: totals by type, by format, accumulated size and current fiscal period. Returned by `GET /v1/tax_reports/stats`. - `object` (string, required, enum: `tax_report_stats`) - `total_reports` (integer, required) — Total number of generated reports. - `by_type` (object, required) — Report count by type. - `by_format` (object, required) — Report count by output format. - `total_size_bytes` (integer, required) — Accumulated size (bytes) of all generated files. - `current_quarter` (object, required) — Trimestre fiscal en curso (UTC). - `current_year` (integer, required) — Current fiscal year (UTC). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/tax-catalog — Retrieve the tax catalog - **Operation ID**: `public-api.v1.tax-catalog.show` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.tax-catalog.show Return, in a single document, the Spanish tax knowledge you need to build a compliant invoicing form: indirect tax regimes (IVA, IGIC, IPSI) with their legal rates and their VeriFactu L1 code, header-level operation regimes with the legal wording each one requires, exemption causes with their AEAT code, legal wording and article of the Spanish VAT Act, the system IRPF withholding rates and the closed matrix of legal VAT/equivalence-surcharge pairs. It replaces the hardcoded table every integration ends up maintaining by hand. The catalog carries no data of the authenticated company: two different companies receive byte-identical bodies for the same language, and `retention_rates` never includes the custom taxes a company creates through `POST /v1/taxes`. Withholding rates are published in POSITIVE, so apply them as a deduction from the taxable base. This is the catalog of what the platform supports, NOT an exhaustive normative list of every regime, exemption or withholding rate Spanish law defines. Use it to know what you can send to this API; do not read it as tax advice or as a substitute for the legislation. Every entry carries its `label` (and, in the two normative blocks, its `description`) in Spanish, English and Catalan at once. `Accept-Language` only picks the language reported in `primary_language`; it never filters the payload, so one cached document is enough to render a multilingual selector. The response is cacheable: it ships `ETag` and a public `Cache-Control`, and sending the validator back in `If-None-Match` returns 304 with no body. Two languages produce two different `ETag`s, because the negotiated language travels inside the body. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TaxCatalog), required) — The Spanish tax catalog as a single document: indirect tax regimes with their legal rates and VeriFactu code, header-level operation regimes, exemption causes with their article and legal wording, system withholding rates, and the legal VAT/equivalence-surcharge pairs. It carries no data of the authenticated company, so two different companies receive byte-identical bodies for the same language, and it is not an entity: it has no `id` and no `object`. - `primary_language` (string, required, enum: `es`, `en`, `ca`) — Language resolved from `Accept-Language` (`es`, `en` or `ca`; `es` when the header is absent or names no supported language). It declares the primary language of the document; it does not filter it — every entry always carries all three translations. It travels inside the body on purpose, which is why two languages yield different `ETag`s. - `indirect_tax_regimes` (array, required) — Indirect tax regimes (IVA, IGIC, IPSI) with their legal rates, standard rate, VeriFactu L1 code and the AEAT zones that determine each one. - `operation_regimes` (array, required) — Header-level operation regimes with the legal wording each one requires and whether it forces a zero VAT amount. - `exemption_causes` (array, required) — Exemption, non-subjection and special-regime causes with their AEAT codes, legal wording, applicable article and whether they can be declared per line. - `retention_rates` (array, required) — System withholding rates, expressed in positive. Mostly IRPF withholdings, plus one contractual retainer that is not a tax and one IRNR rate (see the entry schema). The catalog of what the platform offers, not an exhaustive list of every rate Spanish law defines. - `equivalence_surcharge_pairs` (array, required) — Closed matrix of the legal VAT/equivalence-surcharge pairs of article 161 of the Spanish VAT Act. - **304** — The catalog has not changed since the validator you sent in `If-None-Match`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/active — List active taxes - **Operation ID**: `public-api.v1.taxes.active` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.active Return the active taxes available to your company, combining system-wide defaults plus company-specific definitions. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/by-type — List taxes filtered by type - **Operation ID**: `public-api.v1.taxes.by_type` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.by_type Return taxes filtered by category via the type query param (vat, retention, surcharge, other). Defaults to vat when omitted. ## Query parameters - `type` (string, optional, default: `"vat"`) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/taxes/calculate — Calculate a tax over a base amount - **Operation ID**: `public-api.v1.taxes.calculate` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.calculate Apply the referenced tax to a base amount and return the breakdown: base, tax_rate, tax_amount, total_amount and the full tax object. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `base`, `taxes_id`. Public REST API v1 — POST /v1/taxes/calculate. Body: `{ base: float, taxes_id: string }`. `taxes_id` es la FK a la tabla global `taxes` (valor UUID v7) — plural (D1), NUNCA `tax_id` (NIF/CIF fiscal). Devuelve `{ base, tax_rate, tax_amount, total_amount, tax }`. - `base` (number, required, min 0) - `taxes_id` (string, required, format: uuid) — Identifier (UUID v7) of the tax to apply, from the global `taxes` catalog. A well-formed but non-existent value returns 404 `tax_not_found`. ## Responses - **200** - Body (`application/json`): - `data` (object (TaxCalculation), required) — Resultado de aplicar un tax a un importe base. Devuelto por `POST /v1/taxes/calculate`. - `base` (number, required) — Base amount the tax is applied to (EUR). - `tax_rate` (number, required) — Applied tax rate, e.g. `21` for 21% VAT. - `tax_amount` (number, required) — Tax amount (base × tax_rate / 100). - `total_amount` (number, required) — Resulting total amount (base + tax_amount). - `tax` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/taxes/calculate-totals — Calculate totals for a set of lines - **Operation ID**: `public-api.v1.taxes.calculate_totals` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.calculate_totals Compute subtotal, VAT, surcharge, retention and grand total for an array of line items with quantity, price, discount and tax rates. Returns the document totals plus the per-line breakdown. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `lines`. Public REST API v1 — POST /v1/taxes/calculate-totals. Body: `{ lines: [{ quantity, unit_price, discount?, vat_rate?, retention_rate?, surcharge_rate? }] }`. Returns subtotal, VAT, surcharge, withholding and total. The canonical field is `unit_price` (aligned with Invoice/Quote lines). `price` is accepted as a legacy alias so as not to break integrators that already send the previous shape; the controller normalizes it to `unit_price`. - `lines` (array, required, maxItems 500) - `quantity` (number, required) - `unit_price` (number, required) - `discount` (number | null, optional, min 0, max 100) - `vat_rate` (number | null, optional, min 0) - `retention_rate` (number | null, optional) - `surcharge_rate` (number | null, optional, min 0) ## Responses - **200** - Body (`application/json`): - `data` (object (TaxTotals), required) — Aggregated totals of a set of lines. Returned by `POST /v1/taxes/calculate-totals`. Total formula: `subtotal + total_vat + total_surcharge − total_retention`. - `subtotal` (number, required) — Sum of the taxable bases of all lines (EUR). - `total_vat` (number, required) — Sum of the VAT of all lines. - `total_surcharge` (number, required) — Sum of the equivalence surcharge of all lines. - `total_retention` (number, required) — Sum of the withholding (IRPF) of all lines. - `total` (number, required) — Total general. - `lines` (array, required) — Calculated breakdown for each received line, in the same order. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/taxes — Create a tax - **Operation ID**: `public-api.v1.taxes.create` - **Tag**: Taxes - **Required scope**: `taxes:write` — Create and update taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.create Register a new tax with name, unique code, type (vat, retention, surcharge or other), rate and scope (sale, purchase or both). The ISO-2 country code is required. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 17 properties; 6 required: `name`, `code`, `type`, `rate`, `applies_to`, `country`. - `name` (string, required, maxLength 255) - `code` (string, required, maxLength 50) - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) - `rate` (number, required) - `applies_to` (string, required, enum: `sale`, `purchase`, `both`) - `is_active` (boolean, optional) - `country` (string, required, maxLength 2, minLength 2) - `description` (string | null, optional, maxLength 1000) - `customer_visible_label` (string | null, optional) — Custom label for this tax shown to the customer on documents (e.g. "IVA 21% incluido"). Up to 200 characters. - `external_reference` (string | null, optional, maxLength 10) - `valid_from` (string | null, optional, format: date) - `valid_until` (string | null, optional, format: date) — End date of the tax validity window (`YYYY-MM-DD`). Must be on or after `valid_from`. - `reverse_charge` (boolean, optional) - `country_aeat_zone` (string | null, optional, enum: `peninsula`, `canarias`, `ceuta`, `melilla`) - `linked_surcharge_taxes_id` (string | null, optional) — FK to the linked equivalence-surcharge tax. UUID v7 value referencing the global `taxes` catalog. - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `default_for_documents` (object, optional) — Sets this tax as the default per document type. Object with optional booleans: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. - `invoice` (boolean, optional) - `quote` (boolean, optional) - `delivery_note` (boolean, optional) - `proforma` (boolean, optional) - `purchase_invoice` (boolean, optional) - `recurring_invoice` (boolean, optional) ## Responses - **201** — Tax created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/defaults/{docType} — Get default taxes for a document type - **Operation ID**: `public-api.v1.taxes.defaults` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.defaults Return the configured default taxes (vat, retention, surcharge) for the given document type, scoped to your company. Each slot is either a Tax or null when no default is configured. ## Path parameters - `docType` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TaxDefaultsForDocuments), required) — Defaults grouped by tax category for a given document type. Each key contains the (possibly empty) list of taxes marked as default for that `docType` within its `type`. - `vat` (array, required) — Taxes with `type=vat` that are default for the requested docType. - `retention` (array, required) - `surcharge` (array, required) - `other` (array, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/taxes/{tax} — Delete a tax - **Operation ID**: `public-api.v1.taxes.delete` - **Tag**: Taxes - **Required scope**: `taxes:delete` — Delete taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.delete Delete a tax. Fails with 409 if the tax is referenced by existing documents. System taxes (is_system=true) cannot be deleted. ## Path parameters - `tax` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/for-purchases — List taxes applicable to purchases - **Operation ID**: `public-api.v1.taxes.for_purchases` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.for_purchases Return the taxes available for purchase documents (supplier invoices). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/for-sales — List taxes applicable to sales - **Operation ID**: `public-api.v1.taxes.for_sales` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.for_sales Return the taxes available for sales documents (invoices, quotes, proformas, delivery notes). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/{tax}/is-in-use — Check whether a tax is in use - **Operation ID**: `public-api.v1.taxes.is_in_use` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.is_in_use Return whether the tax is referenced by existing documents. Useful for safe-deletion checks before calling DELETE. ## Path parameters - `tax` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** — `TaxUsageV1Resource` - Body (`application/json`): - `data` (object (TaxUsage), required) — Desglose del uso de un tax across bounded contexts. Permite decidir si es seguro borrar o desactivar un tax (`in_use=false` ⇒ delete seguro). - `object` (string, required, enum: `tax_usage`) - `taxes_id` (string, required) — UUID (v7) of the requested tax. - `in_use` (boolean, required) — true si `total_count > 0`. - `total_count` (integer, required, min 0) — Aggregate sum of the 6 keys in `used_by`. - `used_by` (object, required) — Count by consumer BC. Excludes `purchase_invoice_lines` (no FK) and `recurring_invoices` (JSON lines). - `taxes_id` (string, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes — List all taxes - **Operation ID**: `public-api.v1.taxes.list` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.list List the tax rates available to your company (Spanish IVA, IRPF, recargo, etc.). ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `type` (string, optional) — Tax type: `vat`, `retention`, `surcharge`, `other`. - `type[in]` (string, optional) — Tax type: `vat`, `retention`, `surcharge`, `other`. - `is_active` (boolean, optional) — Filter active / inactive taxes. - `is_default` (boolean, optional) — Filter taxes marked as global default (legacy flag). - `applies_to` (string, optional) — Scope: `sales`, `purchases`, `both`. - `applies_to[in]` (string, optional) — Scope: `sales`, `purchases`, `both`. - `country` (string, optional) — ISO 3166-1 alpha-2 country code. - `code` (string, optional) — Short tax code (exact match). - `search` (string, optional) — Escaped LIKE search over `name` and `code` (limit 80 chars). - `external_reference` (string, optional) — Exact AEAT key: S1..S3, E1..E6, N1..N2. - `external_reference[in]` (string, optional) — Exact AEAT key: S1..S3, E1..E6, N1..N2. - `is_system` (boolean, optional) — Filter system taxes (global catalog) vs custom. - `country_aeat_zone` (string, optional) — AEAT fiscal zone: `peninsula`, `canarias`, `ceuta`, `melilla`. - `indirect_tax_regime` (string, optional) — Indirect tax regime DERIVED from the AEAT zone: `iva`, `igic`, `ipsi`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/taxes/{tax}/set-default — Mark a tax as the default for its type - **Operation ID**: `public-api.v1.taxes.set_default` - **Tag**: Taxes - **Required scope**: `taxes:write` — Create and update taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.set_default Promote a tax to the system-wide default for its category (vat, retention or surcharge). If another tax was the default for the same type it is demoted automatically. ## Path parameters - `tax` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/taxes/{tax}/set-default/{docType} — Set tax default for a document type - **Operation ID**: `public-api.v1.taxes.set_default_for_document` - **Tag**: Taxes - **Required scope**: `taxes:write` — Create and update taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.set_default_for_document Assign a tax as the default for a specific document type (invoice, quote, proforma, delivery_note, purchase_invoice, recurring_invoice). ## Path parameters - `tax` (string, required) - `docType` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/{tax} — Retrieve a tax - **Operation ID**: `public-api.v1.taxes.show` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.show Retrieve a tax rate by its `uuid`. ## Path parameters - `tax` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/taxes/stats — Get tax stats - **Operation ID**: `public-api.v1.taxes.stats` - **Tag**: Taxes - **Required scope**: `taxes:read` — Read taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.stats Aggregated KPIs for the tax rates available to your company: total tax count, active count, and breakdown by type (vat, retention, surcharge, other). Returned as `{ "data": TaxStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TaxStats), required) — Aggregated KPIs over the company tax catalog (includes global system taxes). Breakdown por `type` y por `external_reference` AEAT. - `object` (string, required, enum: `tax_stats`) - `total` (integer, required, min 0) — Total taxes visible to the company (system + custom). - `active` (integer, required, min 0) — Taxes with `is_active=true`. - `inactive` (integer, required, min 0) — Taxes with `is_active=false`. - `system_count` (integer, required, min 0) — Taxes from the global system catalog (`is_system=true`). - `custom_count` (integer, required, min 0) — Company-specific taxes (`is_system=false`). - `by_type` (object, required) — Tax count by `type`. - `by_aeat_code` (object, required) — Tax count by AEAT `external_reference`. The `"(null)"` key groups taxes without an assigned AEAT code. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/taxes/{tax}/toggle — Toggle tax active state - **Operation ID**: `public-api.v1.taxes.toggle` - **Tag**: Taxes - **Required scope**: `taxes:write` — Create and update taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.toggle Flip a tax between active and inactive. Inactive taxes are hidden from selectors but stay available for already-issued documents. ## Path parameters - `tax` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/taxes/{tax} — Update a tax - **Operation ID**: `public-api.v1.taxes.update` - **Tag**: Taxes - **Required scope**: `taxes:write` — Create and update taxes. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/taxes/public-api.v1.taxes.update Partial update of a tax: name, code, rate, applies_to, country and description. System taxes (is_system=true) are not editable. ## Path parameters - `tax` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 16 properties; none of them required. - `name` (string, optional, maxLength 255) - `code` (string, optional, maxLength 50) - `rate` (number, optional) - `applies_to` (string, optional, enum: `sale`, `purchase`, `both`) - `country` (string | null, optional, maxLength 2, minLength 2) - `description` (string | null, optional, maxLength 1000) - `is_active` (boolean, optional) - `customer_visible_label` (string | null, optional) — Custom label for this tax shown to the customer on documents (e.g. "IVA 21% incluido"). Up to 200 characters. - `external_reference` (string | null, optional, maxLength 10) - `valid_from` (string | null, optional, format: date) - `valid_until` (string | null, optional, format: date) — End date of the tax validity window (`YYYY-MM-DD`). Must be on or after `valid_from`. - `reverse_charge` (boolean, optional) - `country_aeat_zone` (string | null, optional, enum: `peninsula`, `canarias`, `ceuta`, `melilla`) - `linked_surcharge_taxes_id` (string | null, optional) — FK to the linked equivalence-surcharge tax; `null` unlinks it. UUID v7 value referencing the global `taxes` catalog. - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `default_for_documents` (object, optional) — Sets this tax as the default per document type. Object with optional booleans: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. - `invoice` (boolean, optional) - `quote` (boolean, optional) - `delivery_note` (boolean, optional) - `proforma` (boolean, optional) - `purchase_invoice` (boolean, optional) - `recurring_invoice` (boolean, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (Tax), required) — A tax rate configuration. Catalog partially global (`is_system=true` for system taxes, without `company_id`) and partially custom per company. - `id` (string, required) - `object` (string, required, enum: `tax`) - `name` (string, required, maxLength 255) — Human-readable name of the tax (e.g. "IVA 21%"). - `code` (string, required) — Short tax code (e.g. "IVA21"). - `rate` (number, required, format: float) — Applied tax rate, e.g. `21` for 21% VAT. - `type` (string, required, enum: `vat`, `retention`, `surcharge`, `other`) — Tax category: `vat` (VAT), `retention` (IRPF withholding), `surcharge` (equivalence surcharge), `other`. - `applies_to` (string, required, enum: `sales`, `purchases`, `both`) — Scope where it applies: `sales` (sales only), `purchases` (purchases only), `both` (both). - `country` (string | null, required) — ISO 3166-1 alpha-2 country code (e.g. `ES`). - `is_default` (boolean, required) — true if this is the default tax for its `type` (independent of the `default_for_documents` map). - `is_active` (boolean, required) — Indicates whether the tax is enabled for use on new documents. - `is_system` (boolean, required) — true if the tax is from the global system catalog (not editable, without `company_id`). - `description` (string | null, required, maxLength 1000) — Free-text description of the tax. - `default_for_documents` (object, required) — Map `DocumentType -> bool` indicating for which document types this tax is default. Replaces the 5 legacy booleans `is_default_invoice/quote/delivery_note/proforma/purchase_invoice`. - `customer_visible_label` (string | null, required, maxLength 200) — Tax label shown to the end customer on documents (PDF, public link, email). - `external_reference` (string | null, required, enum: `S1`, `S2`, `S3`, `E1`, `E2`, `E3`, `E4`, `E5`, `E6`, `N1`, `N2`, `null`) — AEAT-SII / VeriFactu key (closed catalog: S1..S3 subject, E1..E6 exempt, N1..N2 not subject). - `valid_from` (string | null, required, format: date) — Legal effective start date of the rate (BOE). Format `YYYY-MM-DD`. - `valid_until` (string | null, required, format: date) — Legal effective end date of the rate. Invariant: `valid_until >= valid_from` when both are non-null. - `reverse_charge` (boolean, required) — Reverse charge (Art. 84.Uno.2º LIVA). Default `false`. - `country_aeat_zone` (string | null, required, enum: `peninsula`, `canarias`, `ceuta`, `melilla`, `null`) — Zona AEAT a efectos fiscales: `peninsula` + Baleares, `canarias` (IGIC), `ceuta` (IPSI), `melilla` (IPSI). - `indirect_tax_regime` (string | null, required, enum: `iva`, `igic`, `ipsi`, `null`) — Indirect tax regime DERIVED from the AEAT zone, read-only: `iva` (Península), `igic` (Canarias), `ipsi` (Ceuta/Melilla). Only set for `type=vat`; `null` otherwise. - `linked_surcharge_taxes_id` (string | null, required) — Identity (UUID v7) of the linked equivalence-surcharge tax. Foreign key to the global `taxes` table (key suffix `_taxes_id` per the UUID policy). `null` when there is no link. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `created_at` (string | null, required, format: date-time) - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-balances/employee/{employee} — Retrieve an employee’s time balance for a period - **Operation ID**: `public-api.v1.time_balances.employee` - **Tag**: Time Balances - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-balances/public-api.v1.time_balances.employee Return the time balance of an arbitrary period of an employee: expected vs worked minutes, the balance and overtime per day, and the period totals. The employee is the `{employee}` (UUID v7) in the path; `from` and `to` (`YYYY-MM-DD`) are required. This is the same contract the monthly close reuses over closed periods. A range where `to` is before `from` returns 422. Totals are in minutes. A computed resource: it exposes `employee_id`, never an `id`. ## Path parameters - `employee` (string, required) ## Query parameters - `from` (string, required, format: date) — Period start date (YYYY-MM-DD). - `to` (string, required, format: date) — Period end date (YYYY-MM-DD). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TimeBalance), required) — The time balance of an arbitrary period (`from`/`to`) of an employee for the Control Horario (time tracking) module. A computed resource with no entity identity: it is keyed by employee + period, so it exposes `employee_id` (UUID v7) and never an `id`. It is the same contract the monthly close reuses over closed periods. Totals are in minutes; `days` is the daily breakdown. - `object` (string, required, enum: `time_balance`) — Always `time_balance`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the balance belongs to. - `from` (string, required, format: date) — Start of the period (YYYY-MM-DD). - `to` (string, required, format: date) — End of the period (YYYY-MM-DD). - `total_expected_minutes` (integer, required) — Total expected working minutes of the period (holidays and approved absences already discounted). - `total_worked_minutes` (integer, required) — Total worked minutes of the period. - `total_balance_minutes` (integer, required) — Period balance in minutes (worked − expected). - `total_overtime_minutes` (integer, required) — Total overtime minutes of the period. - `days` (array, required) — Daily breakdown of the period. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-balances/monthly-sheet — Retrieve an employee’s monthly time sheet - **Operation ID**: `public-api.v1.time_balances.monthly_sheet` - **Tag**: Time Balances - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-balances/public-api.v1.time_balances.monthly_sheet Return the live monthly time sheet of an employee for the open (in-progress) period: expected vs worked minutes, the balance and overtime per day, and the monthly totals. `employee_id` (UUID v7) is required; `month` (`YYYY-MM`) defaults to the current month. The sheet is recomputed on every request from the immutable ledger, so a just-recorded clock entry is reflected without closing the month. Expected minutes discount public holidays and approved absences. Totals are in minutes. A computed resource: it exposes `employee_id`, never an `id`. ## Query parameters - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) whose monthly sheet to retrieve. - `month` (string | null, optional, pattern: `^\d{4}-(0[1-9]|1[0-2])$`) — Sheet month in YYYY-MM format; defaults to the current month. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (MonthlyTimeSheet), required) — The live monthly time sheet of an employee for the open (in-progress) period of the Control Horario (time tracking) module. A computed resource with no entity identity: it is keyed by employee + month, so it exposes `employee_id` (UUID v7) and never an `id`. Totals are in minutes; `days` is the daily breakdown. It is recomputed on every request, so a just-recorded clock entry is reflected without closing the month. - `object` (string, required, enum: `time_sheet`) — Always `time_sheet`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the sheet belongs to. - `month` (string, required) — Month of the sheet (YYYY-MM). - `total_expected_minutes` (integer, required) — Total expected working minutes of the month (holidays and approved absences already discounted). - `total_worked_minutes` (integer, required) — Total worked minutes of the month. - `total_balance_minutes` (integer, required) — Month balance in minutes (worked − expected). - `total_overtime_minutes` (integer, required) — Total overtime minutes of the month. - `days` (array, required) — Daily breakdown of the month. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — Not found - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-balances/team-summary — Retrieve the team time balance summary - **Operation ID**: `public-api.v1.time_balances.team_summary` - **Tag**: Time Balances - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-balances/public-api.v1.time_balances.team_summary Return the team time balance summary (manager view) for a month: one row per active employee with their expected, worked, balance and overtime minutes. `month` (`YYYY-MM`) defaults to the current month. Only active employees with a schedule are included. Totals are in minutes. A computed resource with no `id`. ## Query parameters - `month` (string | null, optional, pattern: `^\d{4}-(0[1-9]|1[0-2])$`) — Summary month in YYYY-MM format; defaults to the current month. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TeamTimeBalanceSummary), required) — The team time balance summary (manager view) for a month for the Control Horario (time tracking) module. A computed resource with no entity identity: it is keyed by company + month and holds one row per active employee with their monthly totals. Totals are in minutes. - `object` (string, required, enum: `team_time_balance_summary`) — Always `team_time_balance_summary`. - `month` (string, required) — Month of the summary (YYYY-MM). - `employees` (array, required) — One row per active employee with their monthly totals. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-corrections/{time_correction}/approve — Approve a time entry correction - **Operation ID**: `public-api.v1.time_corrections.approve` - **Tag**: Time Corrections - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-corrections/public-api.v1.time_corrections.approve Approve a pending correction request by its `id` (UUID v7), appending the resolving `correction_entry` linked to the original time entry. An optional `note` from the approver may be supplied. A request that is not pending returns 422 (already resolved), and approving your own request returns 422 (self-approval is forbidden). Returns 200 with the resolved correction. ## Path parameters - `time_correction` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. - `note` (string | null, optional, maxLength 500) — Optional note from the approver. ## Responses - **200** - Body (`application/json`): - `data` (object (TimeCorrection), required) — A time entry correction request for the Control Horario (time tracking) module (RD-ley 8/2019). A correction is an append-only entry that references the original time entry without mutating it (analogous to a corrective invoice); its status is derived from the ledger — there is no status column. The workflow is request → approve/reject, always resolved by a manager and never self-approved. - `id` (string, required, format: uuid) — Opaque identifier of the correction request, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_correction`) — Always `time_correction`. - `status` (string, required, enum: `pending`, `approved`, `rejected`) — Derived workflow state: `pending` (awaiting resolution), `approved` or `rejected`. - `kind` (string, required, enum: `add_missing_entry`, `adjust_time`, `remove_entry`) — Type of correction: `add_missing_entry`, `adjust_time` or `remove_entry`. - `time_entry_id` (string, required, format: uuid) — UUID v7 of the original time entry the correction refers to. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the corrected entry belongs to. - `requested_by_id` (string, required, format: uuid) — UUID v7 of the user who requested the correction. - `resolved_by_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while it is still pending. - `reason` (string | null, required) — Reason supplied with the request (or the rejection); `null` if none. - `proposed` (object, required) — Map of the proposed values of the correction (shape depends on `kind`). - `resolved_at` (string | null, required, format: date-time) — When the request was resolved (ISO 8601); `null` while it is still pending. - `requested_at` (string, required, format: date-time) — When the correction was requested (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-corrections — Request a time entry correction - **Operation ID**: `public-api.v1.time_corrections.create` - **Tag**: Time Corrections - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-corrections/public-api.v1.time_corrections.create Request the correction of a time entry (RD-ley 8/2019). `time_entry_id` (UUID v7 of the entry to correct), `kind` (`add_missing_entry`/`adjust_time`/`remove_entry`), a `reason` and the `proposed` values are required. A correction is a new append-only entry that references the original entry without mutating it (analogous to a corrective invoice); the workflow stays `pending` until a manager approves or rejects it. Returns 201 with the created request and a `Location` header. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 4 properties; 3 required: `time_entry_id`, `kind`, `reason`. - `time_entry_id` (string, required, format: uuid) — Clock entry ID (UUID v7) of the workday record to correct. - `kind` (string, required, enum: `add_missing_entry`, `adjust_time`, `remove_entry`) — Type of correction: `add_missing_entry`, `adjust_time` or `remove_entry`. - `reason` (string, required, maxLength 500) — Required reason for the correction request. - `proposed` (object, optional) — Proposed values for the correction (consistency validated in the domain). ## Responses - **201** - Body (`application/json`): - `data` (object (TimeCorrection), required) — A time entry correction request for the Control Horario (time tracking) module (RD-ley 8/2019). A correction is an append-only entry that references the original time entry without mutating it (analogous to a corrective invoice); its status is derived from the ledger — there is no status column. The workflow is request → approve/reject, always resolved by a manager and never self-approved. - `id` (string, required, format: uuid) — Opaque identifier of the correction request, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_correction`) — Always `time_correction`. - `status` (string, required, enum: `pending`, `approved`, `rejected`) — Derived workflow state: `pending` (awaiting resolution), `approved` or `rejected`. - `kind` (string, required, enum: `add_missing_entry`, `adjust_time`, `remove_entry`) — Type of correction: `add_missing_entry`, `adjust_time` or `remove_entry`. - `time_entry_id` (string, required, format: uuid) — UUID v7 of the original time entry the correction refers to. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the corrected entry belongs to. - `requested_by_id` (string, required, format: uuid) — UUID v7 of the user who requested the correction. - `resolved_by_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while it is still pending. - `reason` (string | null, required) — Reason supplied with the request (or the rejection); `null` if none. - `proposed` (object, required) — Map of the proposed values of the correction (shape depends on `kind`). - `resolved_at` (string | null, required, format: date-time) — When the request was resolved (ISO 8601); `null` while it is still pending. - `requested_at` (string, required, format: date-time) — When the correction was requested (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-corrections — List all time entry corrections - **Operation ID**: `public-api.v1.time_corrections.list` - **Tag**: Time Corrections - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-corrections/public-api.v1.time_corrections.list List the time entry correction requests of your company with cursor-based pagination, ordered by request time. Supports filtering by `status` (`pending` is the manager inbox, `approved`/`rejected` are resolved), `employee_id` (UUID v7) and a date range (`from`/`to`). ## Query parameters - `status` (string | null, optional, enum: `pending`, `approved`, `rejected`) — Status to filter by: pending / approved / rejected. - `employee_id` (string | null, optional, format: uuid) — Employee ID (UUID v7) to filter by. - `from` (string | null, optional, format: date-time) — Start date/time of the request range (ISO 8601). - `to` (string | null, optional, format: date-time) — End date/time of the request range (ISO 8601). - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the correction request, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_correction`) — Always `time_correction`. - `status` (string, required, enum: `pending`, `approved`, `rejected`) — Derived workflow state: `pending` (awaiting resolution), `approved` or `rejected`. - `kind` (string, required, enum: `add_missing_entry`, `adjust_time`, `remove_entry`) — Type of correction: `add_missing_entry`, `adjust_time` or `remove_entry`. - `time_entry_id` (string, required, format: uuid) — UUID v7 of the original time entry the correction refers to. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the corrected entry belongs to. - `requested_by_id` (string, required, format: uuid) — UUID v7 of the user who requested the correction. - `resolved_by_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while it is still pending. - `reason` (string | null, required) — Reason supplied with the request (or the rejection); `null` if none. - `proposed` (object, required) — Map of the proposed values of the correction (shape depends on `kind`). - `resolved_at` (string | null, required, format: date-time) — When the request was resolved (ISO 8601); `null` while it is still pending. - `requested_at` (string, required, format: date-time) — When the correction was requested (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-corrections/{time_correction}/reject — Reject a time entry correction - **Operation ID**: `public-api.v1.time_corrections.reject` - **Tag**: Time Corrections - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-corrections/public-api.v1.time_corrections.reject Reject a pending correction request by its `id` (UUID v7) with a required `reason`, resolving it without touching the original time entry. A request that is not pending returns 422 (already resolved). Returns 200 with the resolved correction. ## Path parameters - `time_correction` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 1 property; 1 required: `reason`. - `reason` (string, required, maxLength 500) — Required rejection reason. ## Responses - **200** - Body (`application/json`): - `data` (object (TimeCorrection), required) — A time entry correction request for the Control Horario (time tracking) module (RD-ley 8/2019). A correction is an append-only entry that references the original time entry without mutating it (analogous to a corrective invoice); its status is derived from the ledger — there is no status column. The workflow is request → approve/reject, always resolved by a manager and never self-approved. - `id` (string, required, format: uuid) — Opaque identifier of the correction request, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_correction`) — Always `time_correction`. - `status` (string, required, enum: `pending`, `approved`, `rejected`) — Derived workflow state: `pending` (awaiting resolution), `approved` or `rejected`. - `kind` (string, required, enum: `add_missing_entry`, `adjust_time`, `remove_entry`) — Type of correction: `add_missing_entry`, `adjust_time` or `remove_entry`. - `time_entry_id` (string, required, format: uuid) — UUID v7 of the original time entry the correction refers to. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the corrected entry belongs to. - `requested_by_id` (string, required, format: uuid) — UUID v7 of the user who requested the correction. - `resolved_by_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while it is still pending. - `reason` (string | null, required) — Reason supplied with the request (or the rejection); `null` if none. - `proposed` (object, required) — Map of the proposed values of the correction (shape depends on `kind`). - `resolved_at` (string | null, required, format: date-time) — When the request was resolved (ISO 8601); `null` while it is still pending. - `requested_at` (string, required, format: date-time) — When the correction was requested (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-corrections/{time_correction} — Retrieve a time entry correction - **Operation ID**: `public-api.v1.time_corrections.show` - **Tag**: Time Corrections - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-corrections/public-api.v1.time_corrections.show Retrieve a single correction request by its `id` (UUID v7), including its derived status. A request belonging to another company returns 404 `correction_request_not_found` (anti-enumeration). ## Path parameters - `time_correction` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TimeCorrection), required) — A time entry correction request for the Control Horario (time tracking) module (RD-ley 8/2019). A correction is an append-only entry that references the original time entry without mutating it (analogous to a corrective invoice); its status is derived from the ledger — there is no status column. The workflow is request → approve/reject, always resolved by a manager and never self-approved. - `id` (string, required, format: uuid) — Opaque identifier of the correction request, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_correction`) — Always `time_correction`. - `status` (string, required, enum: `pending`, `approved`, `rejected`) — Derived workflow state: `pending` (awaiting resolution), `approved` or `rejected`. - `kind` (string, required, enum: `add_missing_entry`, `adjust_time`, `remove_entry`) — Type of correction: `add_missing_entry`, `adjust_time` or `remove_entry`. - `time_entry_id` (string, required, format: uuid) — UUID v7 of the original time entry the correction refers to. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the corrected entry belongs to. - `requested_by_id` (string, required, format: uuid) — UUID v7 of the user who requested the correction. - `resolved_by_id` (string | null, required, format: uuid) — UUID v7 of the user who approved or rejected the request; `null` while it is still pending. - `reason` (string | null, required) — Reason supplied with the request (or the rejection); `null` if none. - `proposed` (object, required) — Map of the proposed values of the correction (shape depends on `kind`). - `resolved_at` (string | null, required, format: date-time) — When the request was resolved (ISO 8601); `null` while it is still pending. - `requested_at` (string, required, format: date-time) — When the correction was requested (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-entries/chain/validate — Validate the time record hash chain - **Operation ID**: `public-api.v1.time_entries.chain.validate` - **Tag**: Time Entries - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.chain.validate Recompute the SHA-256 hash chain (`huella`) of your company time record ledger and compare it against the persisted values without mutating data. Returns whether the chain is intact and, if not, the `id` (UUID v7) of the first corrupted record — the fingerprints themselves are never exposed. Rate-limited to 1 request/minute and rejected with 422 `dataset_too_large` for datasets over 50,000 records. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TimeRecordChainValidation), required) — Result of recomputing and verifying the SHA-256 hash chain (`huella`) of your company time record ledger without mutating data. Returned by `GET /v1/time-entries/chain/validate`. Fingerprints are never exposed. - `object` (string, required, enum: `time_record_chain_validation`) - `is_valid` (boolean, required) — Indicates whether the hash chain is intact. - `total_records` (integer, required) — Total number of time record entries considered. - `validated_records` (integer, required) — Number of validated entries. - `first_invalid_record_id` (string | null, required) — UUID (v7) of the first invalid entry, or `null` if the chain is intact. - `validation_year` (integer, required) — Validation year. - `validated_at` (string, required, format: date-time) — When the validation was run. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-entries/clock-in — Clock in an employee - **Operation ID**: `public-api.v1.time_entries.clock_in` - **Tag**: Time Entries - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.clock_in Clock the start of an employee’s workday, opening a new work span. `employee_id` (UUID v7) and `source` (`web`/`mobile`) are required; `occurred_at` defaults to the server time. Valid only when the employee is not already clocked in; an invalid transition returns 422 in Spanish. Returns 201 with the created `clock_in` entry. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `employee_id`, `source`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) clocking in or out. - `source` (string, required, enum: `web`, `mobile`) — Origin of the clock entry: `web` or `mobile`. - `occurred_at` (string | null, optional, format: date-time) — Clock entry timestamp in ISO 8601 (defaults to the server instant). ## Responses - **201** - Body (`application/json`): - `data` (object (TimeEntry), required) — A single entry of the immutable time record ledger (a clock event) for the Control Horario (time tracking) module. Every clock-in, pause, resume and clock-out — live or retroactive — is an entry chained by fingerprint. The workday state is derived from these entries; there is no session table. Fingerprints are never exposed. - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-entries/clock-out — Clock out an employee - **Operation ID**: `public-api.v1.time_entries.clock_out` - **Tag**: Time Entries - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.clock_out Clock the end of the employee’s current work span (from `working` or `paused`). `employee_id` (UUID v7) and `source` are required. Valid only when a span is open; an invalid transition returns 422 in Spanish. Returns 201 with the created `clock_out` entry. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `employee_id`, `source`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) clocking in or out. - `source` (string, required, enum: `web`, `mobile`) — Origin of the clock entry: `web` or `mobile`. - `occurred_at` (string | null, optional, format: date-time) — Clock entry timestamp in ISO 8601 (defaults to the server instant). ## Responses - **201** - Body (`application/json`): - `data` (object (TimeEntry), required) — A single entry of the immutable time record ledger (a clock event) for the Control Horario (time tracking) module. Every clock-in, pause, resume and clock-out — live or retroactive — is an entry chained by fingerprint. The workday state is derived from these entries; there is no session table. Fingerprints are never exposed. - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-entries/current — Retrieve an employee’s current workday state - **Operation ID**: `public-api.v1.time_entries.current` - **Tag**: Time Entries - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.current Return the derived state of an employee’s current workday (`not_started`/`working`/`paused`/`finished`), reconstructed from the open work span in the immutable ledger — there is no session table. `employee_id` (UUID v7) is required as a query parameter. ## Query parameters - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) whose workday status is queried. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WorkdaySessionStatus), required) — The derived state of an employee’s current workday for the Control Horario (time tracking) module, reconstructed from the open work span in the immutable ledger. There is no session table — the state is computed from the entries. - `object` (string, required, enum: `workday_session`) — Always `workday_session`. - `status` (string, required, enum: `not_started`, `working`, `paused`, `finished`) — Derived workday state: `not_started`, `working`, `paused` or `finished`. - `since` (string | null, required, format: date-time) — Instant of the last event of the open span (when the current state was entered), ISO 8601; `null` if the workday has not started. - `last_entry_type` (string | null, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`, `null`) — Type of the last event of the open span; `null` if the workday has not started. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-entries — List all time entries - **Operation ID**: `public-api.v1.time_entries.list` - **Tag**: Time Entries - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.list List the time entries of your company with cursor-based pagination, ordered by `occurred_at`. Supports filtering by `employee_id` (UUID v7), a date range (`from`/`to`) and `entry_type` (`clock_in`/`pause_start`/`pause_end`/`clock_out`). ## Query parameters - `employee_id` (string | null, optional, format: uuid) — Employee ID (UUID v7) to filter by. - `from` (string | null, optional, format: date-time) — Start date/time of the range (ISO 8601). - `to` (string | null, optional, format: date-time) — End date/time of the range (ISO 8601). - `entry_type` (string | null, optional, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Clock event type to filter by. - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-entries/manual — Record a manual retroactive entry - **Operation ID**: `public-api.v1.time_entries.manual` - **Tag**: Time Entries - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.manual Record a past work span for an employee (a retroactive manual entry). `employee_id` (UUID v7), `started_at`, `ended_at` and a `reason` are required; optional `pauses` add pause intervals. The entries are stored with `is_retroactive: true` and `source: manual`, and one audit log entry is written. `ended_at` before `started_at`, or a missing reason, returns 422 in Spanish. Returns 201 with the created span’s `clock_out` entry. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 5 properties; 4 required: `employee_id`, `started_at`, `ended_at`, `reason`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) the retroactive workday is recorded for. - `started_at` (string, required, format: date-time) — Start of the retroactive workday in ISO 8601. - `ended_at` (string, required, format: date-time) — End of the retroactive workday in ISO 8601 (after the start). - `reason` (string, required, maxLength 500) — Required reason for the retroactive entry. - `pauses` (array | null, optional) — Optional breaks within the retroactive workday. - `start` (string, required, format: date-time) — Start of a break in ISO 8601. - `end` (string, required, format: date-time) — End of a break in ISO 8601 (after its start). ## Responses - **201** - Body (`application/json`): - `data` (object (TimeEntry), required) — A single entry of the immutable time record ledger (a clock event) for the Control Horario (time tracking) module. Every clock-in, pause, resume and clock-out — live or retroactive — is an entry chained by fingerprint. The workday state is derived from these entries; there is no session table. Fingerprints are never exposed. - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-entries/pause — Start a pause - **Operation ID**: `public-api.v1.time_entries.pause` - **Tag**: Time Entries - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.pause Start a pause in the employee’s current work span. `employee_id` (UUID v7) and `source` are required. Valid only when the employee is `working`; an invalid transition returns 422 in Spanish. Returns 201 with the created `pause_start` entry. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `employee_id`, `source`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) clocking in or out. - `source` (string, required, enum: `web`, `mobile`) — Origin of the clock entry: `web` or `mobile`. - `occurred_at` (string | null, optional, format: date-time) — Clock entry timestamp in ISO 8601 (defaults to the server instant). ## Responses - **201** - Body (`application/json`): - `data` (object (TimeEntry), required) — A single entry of the immutable time record ledger (a clock event) for the Control Horario (time tracking) module. Every clock-in, pause, resume and clock-out — live or retroactive — is an entry chained by fingerprint. The workday state is derived from these entries; there is no session table. Fingerprints are never exposed. - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/time-entries/resume — Resume from a pause - **Operation ID**: `public-api.v1.time_entries.resume` - **Tag**: Time Entries - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.resume Resume the employee’s workday after a pause. `employee_id` (UUID v7) and `source` are required. Valid only when the employee is `paused`; an invalid transition returns 422 in Spanish. Returns 201 with the created `pause_end` entry. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 2 required: `employee_id`, `source`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) clocking in or out. - `source` (string, required, enum: `web`, `mobile`) — Origin of the clock entry: `web` or `mobile`. - `occurred_at` (string | null, optional, format: date-time) — Clock entry timestamp in ISO 8601 (defaults to the server instant). ## Responses - **201** - Body (`application/json`): - `data` (object (TimeEntry), required) — A single entry of the immutable time record ledger (a clock event) for the Control Horario (time tracking) module. Every clock-in, pause, resume and clock-out — live or retroactive — is an entry chained by fingerprint. The workday state is derived from these entries; there is no session table. Fingerprints are never exposed. - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-entries/{time_entry} — Retrieve a time entry - **Operation ID**: `public-api.v1.time_entries.show` - **Tag**: Time Entries - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-entries/public-api.v1.time_entries.show Retrieve a single time entry by its `id` (UUID v7). An entry belonging to another company returns 404 `time_record_entry_not_found` (anti-enumeration). ## Path parameters - `time_entry` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TimeEntry), required) — A single entry of the immutable time record ledger (a clock event) for the Control Horario (time tracking) module. Every clock-in, pause, resume and clock-out — live or retroactive — is an entry chained by fingerprint. The workday state is derived from these entries; there is no session table. Fingerprints are never exposed. - `id` (string, required, format: uuid) — Opaque identifier of the time entry, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `time_entry`) — Always `time_entry`. - `employee_id` (string, required, format: uuid) — UUID v7 of the employee the entry belongs to. - `entry_type` (string, required, enum: `clock_in`, `pause_start`, `pause_end`, `clock_out`) — Type of clock event: `clock_in` (opens a work span), `pause_start`, `pause_end` or `clock_out` (closes the span). - `occurred_at` (string, required, format: date-time) — When the clock event happened (ISO 8601). For retroactive entries this is the past instant supplied by the manager. - `recorded_at` (string, required, format: date-time) — When the entry was appended to the ledger (ISO 8601). - `source` (string, required, enum: `web`, `mobile`, `manual`) — Origin of the entry: `web` or `mobile` (live self-service) or `manual` (retroactive entry recorded by a manager). - `is_retroactive` (boolean, required) — Whether the entry was recorded retroactively (a manual entry by a manager) rather than clocked live. - `reason` (string | null, required) — Reason for the retroactive entry; `null` for live entries. - `created_at` (string, required, format: date-time) — Row creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/time-tracking-settings — Retrieve the time tracking settings - **Operation ID**: `public-api.v1.time_tracking_settings.show` - **Tag**: Time Tracking Settings - **Required scope**: `time_entries:read` — Read time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-tracking-settings/public-api.v1.time_tracking_settings.show Return the time tracking configuration of your company: the overtime computation basis (`weekly`/`daily`) and thresholds, the rounding tolerance and the forgotten-clock-in reminder settings. If your company has not configured it yet, the defaults are returned with `id: null` — the first update materialises the row. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (TimeTrackingSettings), required) — The per-company time tracking configuration for the Control Horario module: the overtime computation basis and thresholds, the rounding tolerance and the forgotten-clock-in reminder settings. The public `id` is the UUID v7 of the settings row, or `null` until the first PUT materialises it. - `id` (string | null, required, format: uuid) — UUID v7 of the settings row, or `null` while the company has not materialised it yet (the first PUT creates the row). - `object` (string, required, enum: `time_tracking_settings`) — Always `time_tracking_settings`. - `overtime_basis` (string, required, enum: `weekly`, `daily`) — Overtime computation basis: `weekly` aggregates the week, `daily` computes per day. - `overtime_daily_threshold_minutes` (integer | null, required) — Daily overtime threshold in minutes, or `null` to derive it from the schedule. - `overtime_weekly_threshold_minutes` (integer | null, required) — Weekly overtime threshold in minutes, or `null` to derive it from the schedule. - `overtime_tolerance_minutes` (integer, required) — Rounding tolerance for overtime in minutes. - `clock_in_reminder_enabled` (boolean, required) — Whether the forgotten clock-in reminder is enabled. - `clock_in_reminder_grace_minutes` (integer, required) — Grace minutes after the planned start time before reminding. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/time-tracking-settings — Update the time tracking settings - **Operation ID**: `public-api.v1.time_tracking_settings.update` - **Tag**: Time Tracking Settings - **Required scope**: `time_entries:write` — Create and update time entries. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/time-tracking-settings/public-api.v1.time_tracking_settings.update Create or update the time tracking configuration of your company: `overtime_basis` (`weekly`/`daily`), the optional daily/weekly overtime thresholds in minutes (`null` derives them from the schedule), the rounding `overtime_tolerance_minutes`, and the clock-in reminder toggle and grace minutes. A negative threshold or tolerance returns 422 in Spanish. Returns the updated settings with `id` = UUID v7 of the row. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 6 properties; 4 required: `overtime_basis`, `overtime_tolerance_minutes`, `clock_in_reminder_enabled`, `clock_in_reminder_grace_minutes`. - `overtime_basis` (string, required, enum: `weekly`, `daily`) — Overtime computation basis: `weekly` or `daily`. - `overtime_daily_threshold_minutes` (integer | null, optional, min 0) — Daily overtime threshold in minutes (null = derive from the schedule). - `overtime_weekly_threshold_minutes` (integer | null, optional, min 0) — Weekly overtime threshold in minutes (null = derive from the schedule). - `overtime_tolerance_minutes` (integer, required, min 0) — Overtime rounding tolerance in minutes. - `clock_in_reminder_enabled` (boolean, required) — Whether the missed clock-in reminder is enabled. - `clock_in_reminder_grace_minutes` (integer, required, min 0) — Grace minutes after the scheduled time before reminding. ## Responses - **200** - Body (`application/json`): - `data` (object (TimeTrackingSettings), required) — The per-company time tracking configuration for the Control Horario module: the overtime computation basis and thresholds, the rounding tolerance and the forgotten-clock-in reminder settings. The public `id` is the UUID v7 of the settings row, or `null` until the first PUT materialises it. - `id` (string | null, required, format: uuid) — UUID v7 of the settings row, or `null` while the company has not materialised it yet (the first PUT creates the row). - `object` (string, required, enum: `time_tracking_settings`) — Always `time_tracking_settings`. - `overtime_basis` (string, required, enum: `weekly`, `daily`) — Overtime computation basis: `weekly` aggregates the week, `daily` computes per day. - `overtime_daily_threshold_minutes` (integer | null, required) — Daily overtime threshold in minutes, or `null` to derive it from the schedule. - `overtime_weekly_threshold_minutes` (integer | null, required) — Weekly overtime threshold in minutes, or `null` to derive it from the schedule. - `overtime_tolerance_minutes` (integer, required) — Rounding tolerance for overtime in minutes. - `clock_in_reminder_enabled` (boolean, required) — Whether the forgotten clock-in reminder is enabled. - `clock_in_reminder_grace_minutes` (integer, required) — Grace minutes after the planned start time before reminding. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/invoices/{invoice}/verifactu — Force-create VeriFactu record for invoice - **Operation ID**: `public-api.v1.invoices.verifactu_create` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.invoices.verifactu_create Creates the VeriFactu alta record for an already-issued invoice and enqueues AEAT transmission. Use when automatic creation on send was skipped. ## Path parameters - `invoice` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **201** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The invoice request conflicts with its current state — e.g. an invalid status transition (marking an already-paid invoice as paid), an attempt to edit an issued invoice (use corrective instead), or a reused idempotency key. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/invoices/{invoice}/verifactu — Retrieve invoice VeriFactu record - **Operation ID**: `public-api.v1.invoices.verifactu_get` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.invoices.verifactu_get Returns the VeriFactu (Spanish AEAT SIF) record associated with the invoice if one exists. Responds with `data: null` when the invoice has no record yet. ## Path parameters - `invoice` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (Invoice), required) — A sales invoice (compliant with Spanish AEAT VeriFactu). - `id` (string, required) - `object` (string, required, enum: `invoice`) - `number` (string | null, required) — Definitive invoice number, or `null` for drafts (where `is_number_assigned` is `false`). - `is_number_assigned` (boolean, required) — Whether the invoice has a definitive number assigned. `false` for drafts (where `number` is `null`); becomes `true` after `POST /v1/invoices/{uuid}/assign-real-number`, or automatically on send/payment. - `type` (string, required) — AEAT invoice type code: `F1` (ordinaria), `F2` (simplificada), `F3` (sustitutiva de simplificadas), `R1`–`R5` (rectificativa). - `series` (object (SeriesRef), required) - `client` (object (ClientRef), required) - `status` (string, required) — Invoice lifecycle status. - `issued_on` (string, required, format: date) - `due_on` (string | null, required, format: date) - `subtotal` (number, required) - `taxes_total` (number, required) - `total` (number, required) - `total_disbursements` (number, required) — Sum of the `SUPLIDO` (disbursement) lines of this invoice: amounts the issuer paid in the name and on behalf of the client and re-invoices at cost. Deliberately OUTSIDE `subtotal`, `taxes_total` and `total`, because a disbursement is not part of the issuer's taxable base (art. 78.Tres.3 LIVA) and is not declared in the AEAT VeriFactu record. `0` on an invoice without disbursements. - `total_to_pay` (number, required) — Amount the client actually has to pay: `total + total_disbursements`. DERIVED, never stored — one single formula computes it — and equal to `total` on an invoice without disbursements. Worked example: a 1,000.00 service line at 21% plus a 150.00 `SUPLIDO` line yields `subtotal` 1000.00, `taxes_total` 210.00, `total` 1210.00, `total_disbursements` 150.00 and `total_to_pay` 1360.00. Note that `paid_amount`/`pending_amount` are measured against `total`, not against `total_to_pay`. - `currency` (string, required) — ISO 4217 currency code (always "EUR" in v1). - `notes` (string | null, required) - `external_id` (string | null, required, maxLength 100) — External integration key (ERP/CRM/e-commerce) mapping this document to a record in a third-party system. Free-format, unique per company, filterable via `?external_id=`. `null` when not set. Persistent synchronization key, independent of the request-level `Idempotency-Key`. - `lines` (array, required) - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `tags` (array, required, maxItems 30) — Free classification tags (lowercase slugs `[a-z0-9-]`, ≤ 40 chars each, ≤ 30 tags). Filterable via `?tags[in]=tag1,tag2` (JSON_CONTAINS, OR semantics). Empty `[]` when there are none. - `custom_fields` (array, required, maxItems 50) — Ordered list of typed custom fields `[{field, value}]` (≤ 50). Distinct from `metadata` (a free key→value map): use `custom_fields` for structured, display-oriented integration metadata. Empty `[]` when there are none. - `operation_regime` (string, required) — AEAT VAT operation regime (`general`, `recargo_equivalencia`, `exenta`, etc.). - `exemption_reason` (string | null, required) — VAT exemption cause per document (AEAT catalog E1..E6), or `null` when not exempt. Read-only: the public API derives the AEAT qualification from the regime/cause at issuance. - `legal_mentions` (array, required) — Header legal mentions (includes the mention derived from the exemption cause). Read-only. - `exclude_347` (boolean, required) — Read-only flag: whether this invoice is excluded from the annual Modelo 347 report. The public API cannot mutate it (the create/update FormRequest does not accept it); managing the flag is exclusive to the internal app. - `verifactu_status` (string, required) — Status of the AEAT VeriFactu submission. - `paid_amount` (number, required) — Amount already collected for this invoice (derived from the payment ledger). Satisfies the invariant `paid_amount + pending_amount === total`. - `pending_amount` (number, required) — Outstanding balance pending collection for this invoice (derived from the payment ledger). - `payments` (object, required) — Payment ledger summary, ALWAYS present (never `null`). `total` mirrors `paid_amount`, `pending` mirrors `pending_amount`. `detail` lists the individual payments and is materialized ONLY on the show endpoint (`GET /v1/invoices/{id}`); in list responses `detail` is `[]` (by cost) while `total`/`pending` stay populated. The detail is also available via `GET /v1/invoices/{id}/payments`. - `is_corrective` (boolean, required) — Whether this invoice is a corrective (rectificativa) of another invoice. - `corrective` (object (InvoiceCorrective) | null, required) - `payment` (object (InvoicePayment) | null, required) - `public_link` (object (PublicLink) | null, required) - `substituted_by` (object (InvoiceSubstitutedBy) | null, required) - `recurring` (object (InvoiceRecurring) | null, required) - `paid_at` (string | null, required, format: date-time) - `paid_on` (string | null, required, format: date) - `sent_at` (string | null, required, format: date-time) - `voided_at` (string | null, required, format: date-time) - `void_reason` (string | null, required) - `scheduled_for` (string | null, required, format: date-time) — Scheduled emission timestamp (ISO 8601), or `null` when the invoice is not scheduled. Populated only while `status` is `scheduled`. - `scheduled_action` (string | null, required, enum: `draft`, `issue_and_send`, `null`) — Action the scheduler runs when `scheduled_for` is reached: `issue_and_send` (issue and email) or `draft` (issue only). `null` when the invoice is not scheduled. - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `verifactu_enabled` (boolean, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/aeat-access/records — List AEAT access records - **Operation ID**: `public-api.v1.verifactu.aeat_access.list` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.aeat_access.list Return the dissociated (anonymized) AEAT access ledger with cursor-based pagination. Third-party tax identifiers (NIF) are never exposed; the cursor uses the underlying record UUID v7 only for ordering. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string | null, required) — UUID (v7) of the underlying billing record (used only for ordering/cursoring). - `object` (string, required, enum: `verifactu_aeat_access_record`) - `accessed_at` (string, required, format: date-time) — Timestamp of generation/dissociation of the record. - `accessor_identifier_hash` (string, required) — Anonymized SHA-256 hash of the issuer identifier (the plaintext tax ID is never exposed). - `record_count_disclosed` (integer, required) — Number of dissociated records in this entry. - `disclosure_scope` (string, required) — Scope of the dissociation (`alta` / `anulacion`). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/aeat-access/records/{record} — Retrieve an AEAT access record - **Operation ID**: `public-api.v1.verifactu.aeat_access.show` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.aeat_access.show Retrieve a single dissociated AEAT access record by its `id` (UUID v7). Returns 404 if the record does not exist or belongs to another company. ## Path parameters - `record` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (AeatAccessRecord), required) — A dissociated (anonymized) AEAT access record (REQ-VF-132). Third-party tax identifiers, email and IP are never exposed in plain text — only an anonymized hash. - `id` (string | null, required) — UUID (v7) of the underlying billing record (used only for ordering/cursoring). - `object` (string, required, enum: `verifactu_aeat_access_record`) - `accessed_at` (string, required, format: date-time) — Timestamp of generation/dissociation of the record. - `accessor_identifier_hash` (string, required) — Anonymized SHA-256 hash of the issuer identifier (the plaintext tax ID is never exposed). - `record_count_disclosed` (integer, required) — Number of dissociated records in this entry. - `disclosure_scope` (string, required) — Scope of the dissociation (`alta` / `anulacion`). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/certificates/{certificate}/activate — Activate a company certificate - **Operation ID**: `public-api.v1.verifactu.certificates.activate` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.certificates.activate Make a previously uploaded certificate the active one. Any other active certificate is deactivated atomically. Returns 404 if the certificate does not exist within your company. ## Path parameters - `certificate` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (CompanyCertificate), required) — Metadata of an FNMT (PKCS#12) certificate used to sign VeriFactu transmissions. The certificate password and binary are never exposed — only X.509 metadata. - `id` (string, required) — UUID (v7) of the certificate. - `object` (string, required, enum: `verifactu_certificate`) - `subject_nif` (string, required) — Tax ID (NIF) of the X.509 certificate holder. - `subject_name` (string, required) — Name of the certificate holder. - `subject_kind` (string, required) — Type of holder (individual / legal entity / representative). - `valid_from` (string, required, format: date-time) — Certificate validity start date. - `valid_to` (string, required, format: date-time) — Certificate validity end date (expiration). - `is_active` (boolean, required) — Whether this is the active certificate used to sign transmissions. - `revoked_at` (string | null, required, format: date-time) — Revocation date, or `null` if not revoked. - `created_at` (string, required, format: date-time) — Date the certificate was uploaded. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/certificates/active — Retrieve the active certificate - **Operation ID**: `public-api.v1.verifactu.certificates.active` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.certificates.active Return the currently active FNMT certificate used to sign VeriFactu transmissions. Returns 404 if no certificate has been uploaded yet. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (CompanyCertificate), required) — Metadata of an FNMT (PKCS#12) certificate used to sign VeriFactu transmissions. The certificate password and binary are never exposed — only X.509 metadata. - `id` (string, required) — UUID (v7) of the certificate. - `object` (string, required, enum: `verifactu_certificate`) - `subject_nif` (string, required) — Tax ID (NIF) of the X.509 certificate holder. - `subject_name` (string, required) — Name of the certificate holder. - `subject_kind` (string, required) — Type of holder (individual / legal entity / representative). - `valid_from` (string, required, format: date-time) — Certificate validity start date. - `valid_to` (string, required, format: date-time) — Certificate validity end date (expiration). - `is_active` (boolean, required) — Whether this is the active certificate used to sign transmissions. - `revoked_at` (string | null, required, format: date-time) — Revocation date, or `null` if not revoked. - `created_at` (string, required, format: date-time) — Date the certificate was uploaded. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/certificates — List company certificates - **Operation ID**: `public-api.v1.verifactu.certificates.list` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.certificates.list List the FNMT (PKCS#12) certificates uploaded for your company. The certificate password is never exposed in this representation. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) — UUID (v7) of the certificate. - `object` (string, required, enum: `verifactu_certificate`) - `subject_nif` (string, required) — Tax ID (NIF) of the X.509 certificate holder. - `subject_name` (string, required) — Name of the certificate holder. - `subject_kind` (string, required) — Type of holder (individual / legal entity / representative). - `valid_from` (string, required, format: date-time) — Certificate validity start date. - `valid_to` (string, required, format: date-time) — Certificate validity end date (expiration). - `is_active` (boolean, required) — Whether this is the active certificate used to sign transmissions. - `revoked_at` (string | null, required, format: date-time) — Revocation date, or `null` if not revoked. - `created_at` (string, required, format: date-time) — Date the certificate was uploaded. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/verifactu/certificates/{certificate} — Revoke a company certificate - **Operation ID**: `public-api.v1.verifactu.certificates.revoke` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.certificates.revoke Revoke (delete) a company certificate so it can no longer sign VeriFactu transmissions. Returns 404 if the certificate does not exist within your company. ## Path parameters - `certificate` (string, required) ## Query parameters - `reason` (string, optional) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/certificates — Upload a company certificate - **Operation ID**: `public-api.v1.verifactu.certificates.upload` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.certificates.upload Upload an FNMT certificate (PKCS#12, `.p12`/`.pfx`) as `multipart/form-data` with `certificate_file` and `certificate_password`. The file is validated by magic bytes (ASN.1 DER) and capped at 100 KB; the password is encrypted at rest. The uploaded certificate is activated automatically (previous ones are deactivated). The `Location` header points to `/v1/verifactu/certificates/active`. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `multipart/form-data`, required. 2 properties; 2 required: `certificate_file`, `certificate_password`. - `certificate_file` (string, required, format: binary) — The digital certificate file in PKCS#12 format (`.p12` or `.pfx`). - `certificate_password` (string, required, maxLength 255, minLength 1) ## Responses - **201** — FNMT certificate uploaded and activated successfully. The `Location` header contains the canonical (relative) URL of the active certificate. - Body (`application/json`): - `data` (object (CompanyCertificate), required) — Metadata of an FNMT (PKCS#12) certificate used to sign VeriFactu transmissions. The certificate password and binary are never exposed — only X.509 metadata. - `id` (string, required) — UUID (v7) of the certificate. - `object` (string, required, enum: `verifactu_certificate`) - `subject_nif` (string, required) — Tax ID (NIF) of the X.509 certificate holder. - `subject_name` (string, required) — Name of the certificate holder. - `subject_kind` (string, required) — Type of holder (individual / legal entity / representative). - `valid_from` (string, required, format: date-time) — Certificate validity start date. - `valid_to` (string, required, format: date-time) — Certificate validity end date (expiration). - `is_active` (boolean, required) — Whether this is the active certificate used to sign transmissions. - `revoked_at` (string | null, required, format: date-time) — Revocation date, or `null` if not revoked. - `created_at` (string, required, format: date-time) — Date the certificate was uploaded. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/chain/validate — Validate the VeriFactu hash chain - **Operation ID**: `public-api.v1.verifactu.chain.validate` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.chain.validate Recompute the VeriFactu hash chain (`huella`) for your company and compare it against the persisted values without mutating data. Returns whether the chain is intact and, if not, the first corrupted record. Rate-limited to 1 request/minute and rejected with 422 `dataset_too_large` for datasets over 50,000 records. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (ChainValidation), required) — Result of recomputing and verifying the VeriFactu hash chain (`huella`) of your company without mutating data. Returned by `GET /v1/verifactu/chain/validate`. - `object` (string, required, enum: `verifactu_chain_validation`) - `is_valid` (boolean, required) — Indicates whether the hash chain is intact. - `total_records` (integer, required) — Total number of records considered. - `validated_records` (integer, required) — Number of validated records. - `first_invalid_record_id` (string | null, required) — UUID (v7) of the first invalid record, or `null` if the chain is intact. - `validation_year` (integer, required) — Validated fiscal year. - `validated_at` (string, required, format: date-time) — When the validation was run. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/config — Retrieve VeriFactu config - **Operation ID**: `public-api.v1.verifactu.config` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.config Return the VeriFactu configuration of your company (mode, environment, enrollment status). The certificate password is never exposed. Returned as `{ "data": VeriFactuConfig }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuConfig), required) — VeriFactu configuration of your company (mode, environment, enrollment). Returned by `GET /v1/verifactu/config` and `PUT /v1/verifactu/settings`. The certificate password is never exposed. - `object` (string, required, enum: `verifactu_config`) - `enabled` (boolean, required) — Indicates whether VeriFactu is enabled for the company. - `mode` (string, required) — VeriFactu operation mode. - `auto_transmit` (boolean, required) — Indicates whether transmissions to AEAT are sent automatically. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `notification_emails` (array, required) — Notification emails for VeriFactu events. - `is_locked_until` (string | null, required, format: date-time) — Date until which the mode change is locked, or `null` if not locked. - `has_active_certificate` (boolean, required) — Indica si hay un certificado activo configurado. - `active_certificate_id` (string | null, required) — UUID (v7) of the active certificate, or `null` if there is none. - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/declaracion-responsable — Retrieve the current declaración responsable - **Operation ID**: `public-api.v1.verifactu.declaracion.current` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.declaracion.current Return the current (latest) version of the producer-level VeriFactu Declaración Responsable. Read-only: the declaration is global to the producer of the system (Factuarea), not per-company. Returns 404 `declaracion_not_found` if none has been published. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (DeclaracionResponsable), required) — The producer-level VeriFactu Declaración Responsable (SIF compliance declaration issued by the system producer, Factuarea). Global per `version`, read-only — not per-company. - `id` (string, required) — UUID (v7) of the declaration. - `object` (string, required, enum: `verifactu_declaracion_responsable`) - `version` (string, required) — Version of the declaration (global key). - `system_id` (string, required) — Identifier of the invoicing software system (SIF). - `system_name` (string, required) — Name of the invoicing software system. - `producer_tax_id` (string, required) — Tax ID (NIF) of the SIF producer. - `producer_name` (string, required) — Name of the SIF producer. - `verifactu_only` (boolean, required) — Indicates whether the system operates only in VeriFactu mode (tipo_uso = "S"). - `multi_ot` (boolean, required) — Indicator of multiple taxpayers. - `declared_at` (string, required, format: date-time) — Date of the declaration. - `signing_place` (string, required) — Place where the declaration is signed. - `content_hash` (string, required) — SHA-256 hash of the declaration content for integrity verification (the full content is not exposed). - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/declaracion-responsable/history — List declaración responsable history - **Operation ID**: `public-api.v1.verifactu.declaracion.history` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.declaracion.history Return every version of the producer-level VeriFactu Declaración Responsable (the SIF compliance declaration issued by Factuarea), ordered by `version` descending. Read-only: the declaration is global to the producer of the system, not per-company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) — UUID (v7) of the declaration. - `object` (string, required, enum: `verifactu_declaracion_responsable`) - `version` (string, required) — Version of the declaration (global key). - `system_id` (string, required) — Identifier of the invoicing software system (SIF). - `system_name` (string, required) — Name of the invoicing software system. - `producer_tax_id` (string, required) — Tax ID (NIF) of the SIF producer. - `producer_name` (string, required) — Name of the SIF producer. - `verifactu_only` (boolean, required) — Indicates whether the system operates only in VeriFactu mode (tipo_uso = "S"). - `multi_ot` (boolean, required) — Indicator of multiple taxpayers. - `declared_at` (string, required, format: date-time) — Date of the declaration. - `signing_place` (string, required) — Place where the declaration is signed. - `content_hash` (string, required) — SHA-256 hash of the declaration content for integrity verification (the full content is not exposed). - `created_at` (string, required, format: date-time) - `total_count` (integer, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/events — List VeriFactu events - **Operation ID**: `public-api.v1.verifactu.events.list` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.events.list List the VeriFactu SIF events of your company (alta/anulación transmissions, retries, AEAT responses) with cursor-based pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) — UUID (v7) of the event. - `object` (string, required, enum: `verifactu_event`) - `event_type` (string, required) — SIF event type. - `event_data` (object, required) — Non-PII descriptive data of the event (label, description, CSV, error code/message). Empty object `{}` when there is no data. Sanitized via MetadataSanitizer. - `status` (string, required) — AEAT status of the event. - `occurred_at` (string, required, format: date-time) — When the event was generated. - `created_at` (string, required, format: date-time) - `retry_count` (integer, required) — Number of transmission attempts made. - `max_retries` (integer, required) — Maximum number of retries configured. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/events/{event}/retry — Retry a VeriFactu event - **Operation ID**: `public-api.v1.verifactu.events.retry` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.events.retry Re-queue the AEAT transmission of a failed VeriFactu event. Returns 404 if the event does not exist, 422 `business_rule_violation` / `event_already_processed` if it was already accepted, and 422 `max_retries_exceeded` once the retry limit is reached. ## Path parameters - `event` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `id` (string, required) - `message` (string, required, const: `Transmisión de evento encolada.`) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/events/{event} — Retrieve a VeriFactu event - **Operation ID**: `public-api.v1.verifactu.events.show` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.events.show Retrieve a single VeriFactu SIF event by its `id` (UUID v7). Returns 404 if the event does not exist or belongs to another company. ## Path parameters - `event` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuEvent), required) — A VeriFactu SIF event (Art. 8.1.b): a transmission/retry/AEAT-response entry of the system. The hash chain is never exposed here. - `id` (string, required) — UUID (v7) of the event. - `object` (string, required, enum: `verifactu_event`) - `event_type` (string, required) — SIF event type. - `event_data` (object, required) — Non-PII descriptive data of the event (label, description, CSV, error code/message). Empty object `{}` when there is no data. Sanitized via MetadataSanitizer. - `status` (string, required) — AEAT status of the event. - `occurred_at` (string, required, format: date-time) — When the event was generated. - `created_at` (string, required, format: date-time) - `retry_count` (integer, required) — Number of transmission attempts made. - `max_retries` (integer, required) — Maximum number of retries configured. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/events/summary — Get VeriFactu event summary - **Operation ID**: `public-api.v1.verifactu.events.summary` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.events.summary Return an aggregated summary of your VeriFactu SIF events grouped by type and outcome. Useful for dashboards. Returned as `{ "data": VeriFactuEventSummary }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuEventSummary), required) — Resumen agregado de los eventos del SIF de la empresa: total y desglose por tipo y por estado. Devuelto por `GET /v1/verifactu/events/summary`. - `object` (string, required, enum: `verifactu_event_summary`) - `total_events` (integer, required) — Total de eventos registrados. - `events_by_type` (object, required) — Event count by type. - `events_by_status` (object, required) — Event count by AEAT status. - `last_event_at` (string | null, required, format: date-time) — Date of the last event, or `null` if there is none. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/records/{record}/activities — List VeriFactu record activity timeline - **Operation ID**: `public-api.v1.verifactu.records.activities` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.activities Return the audit timeline for a single VeriFactu record (creation, transmission attempts, AEAT acceptance/rejection). Paginated with a page-number cursor. ## Path parameters - `record` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `object` (string, required, enum: `verifactu_activity`) - `event_type` (string, required) — Tipo de evento de dominio (p. ej. `verifactu.record_created`, `verifactu.transmission_accepted`). - `description` (string, required) — Human-readable description of the event in Spanish. - `metadata` (object, required) — Event metadata. Internal identifiers (PKs), `huella` and certificate secrets are stripped; `*_uuid` values are preserved. - `performed_by` (object | null, required) — Actor that originated the event. `{type:"user",...}` for an internal user, `{type:"api_key",...}` when performed via the public v1 API, or `null` when the event is system-generated (scheduler, periodic sweep) with no attributable actor. - `created_at` (string, required, format: date-time) — When the event occurred (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/records/find-by-csv — Find a VeriFactu record by AEAT CSV - **Operation ID**: `public-api.v1.verifactu.records.find_by_csv` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.find_by_csv Look up a VeriFactu record by the `aeat_csv` (Código Seguro de Verificación) returned by AEAT on acceptance, sent in the JSON body. Returns the matching record or 404 `verifactu_record_not_found`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. Look up a VeriFactu record by its AEAT CSV. Send `{ "aeat_csv": "..." }`; the value is normalized (hyphens removed, upper-cased) before matching. Returns 404 when no record matches. - `aeat_csv` (string, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuRecord), required) — A VeriFactu (Spanish AEAT SIF) record (alta/anulación) associated with an invoice. Includes the hash chain (`huella`), AEAT CSV and transmission status for reconciliation. - `id` (string, required) — UUID (v7) of the VeriFactu record. - `object` (string, required, enum: `verifactu_record`) - `type` (string, required) — Tipo de registro (`alta` / `anulacion`). - `invoice_type` (string, required) — AEAT invoice type (F1, F2, F3, R1-R5, …). - `invoice_number` (string, required) — Number of the associated invoice. - `date` (string, required) — Record date. - `amount` (number, required) — Total amount of the associated invoice. - `status` (string, required) — Transmission status (pending, submitted, accepted, rejected, error). - `huella` (string, required) — Chained SHA-256 fingerprint of the record (VeriFactu fingerprint). - `aeat_submission_id` (string | null, required) — Submission identifier assigned by AEAT (column `aeat_submission_id`). `null` until AEAT returns it. Never the internal PK of the record. - `aeat_csv` (string | null, required) — Secure Verification Code (Código Seguro de Verificación) returned by AEAT on acceptance. `null` until AEAT assigns it. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `transmitted_at` (string | null, required, format: date-time) — ISO 8601 date of the last transmission that reached AEAT. `null` in PENDING/REJECTED/ERROR states. - `is_simplificada` (boolean, required) — Whether the invoice is simplified (F2). - `is_substitute_for_simplified` (boolean, required) — Whether it substitutes one or more simplified invoices (F3). - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/records/find-by-huella — Find a VeriFactu record by hash - **Operation ID**: `public-api.v1.verifactu.records.find_by_huella` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.find_by_huella Look up a VeriFactu record by its `huella` (the chained SHA-256 fingerprint sent in the JSON body). Returns the matching record or 404 `verifactu_record_not_found` if none exists within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. - `huella` (string, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuRecord), required) — A VeriFactu (Spanish AEAT SIF) record (alta/anulación) associated with an invoice. Includes the hash chain (`huella`), AEAT CSV and transmission status for reconciliation. - `id` (string, required) — UUID (v7) of the VeriFactu record. - `object` (string, required, enum: `verifactu_record`) - `type` (string, required) — Tipo de registro (`alta` / `anulacion`). - `invoice_type` (string, required) — AEAT invoice type (F1, F2, F3, R1-R5, …). - `invoice_number` (string, required) — Number of the associated invoice. - `date` (string, required) — Record date. - `amount` (number, required) — Total amount of the associated invoice. - `status` (string, required) — Transmission status (pending, submitted, accepted, rejected, error). - `huella` (string, required) — Chained SHA-256 fingerprint of the record (VeriFactu fingerprint). - `aeat_submission_id` (string | null, required) — Submission identifier assigned by AEAT (column `aeat_submission_id`). `null` until AEAT returns it. Never the internal PK of the record. - `aeat_csv` (string | null, required) — Secure Verification Code (Código Seguro de Verificación) returned by AEAT on acceptance. `null` until AEAT assigns it. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `transmitted_at` (string | null, required, format: date-time) — ISO 8601 date of the last transmission that reached AEAT. `null` in PENDING/REJECTED/ERROR states. - `is_simplificada` (boolean, required) — Whether the invoice is simplified (F2). - `is_substitute_for_simplified` (boolean, required) — Whether it substitutes one or more simplified invoices (F3). - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/records/find-by-invoice-number — Find a VeriFactu record by invoice number - **Operation ID**: `public-api.v1.verifactu.records.find_by_invoice_number` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.find_by_invoice_number Look up the VeriFactu record associated with a given invoice number (sent in the JSON body). Returns the matching record or 404 `verifactu_record_not_found` if the invoice has no record within your company. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 2 properties; none of them required. - `series` (string, optional) - `number` (string, optional) ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuRecord), required) — A VeriFactu (Spanish AEAT SIF) record (alta/anulación) associated with an invoice. Includes the hash chain (`huella`), AEAT CSV and transmission status for reconciliation. - `id` (string, required) — UUID (v7) of the VeriFactu record. - `object` (string, required, enum: `verifactu_record`) - `type` (string, required) — Tipo de registro (`alta` / `anulacion`). - `invoice_type` (string, required) — AEAT invoice type (F1, F2, F3, R1-R5, …). - `invoice_number` (string, required) — Number of the associated invoice. - `date` (string, required) — Record date. - `amount` (number, required) — Total amount of the associated invoice. - `status` (string, required) — Transmission status (pending, submitted, accepted, rejected, error). - `huella` (string, required) — Chained SHA-256 fingerprint of the record (VeriFactu fingerprint). - `aeat_submission_id` (string | null, required) — Submission identifier assigned by AEAT (column `aeat_submission_id`). `null` until AEAT returns it. Never the internal PK of the record. - `aeat_csv` (string | null, required) — Secure Verification Code (Código Seguro de Verificación) returned by AEAT on acceptance. `null` until AEAT assigns it. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `transmitted_at` (string | null, required, format: date-time) — ISO 8601 date of the last transmission that reached AEAT. `null` in PENDING/REJECTED/ERROR states. - `is_simplificada` (boolean, required) — Whether the invoice is simplified (F2). - `is_substitute_for_simplified` (boolean, required) — Whether it substitutes one or more simplified invoices (F3). - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/records — List VeriFactu records - **Operation ID**: `public-api.v1.verifactu.records.list` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.list List the VeriFactu (Spanish AEAT SIF) records of your company with cursor-based pagination. Each record captures the alta/anulación submitted to AEAT, its hash chain (`huella`), `aeat_csv`, and transmission status. Supports filtering by `status`, `type`, `date_from`/`date_to`, and `environment`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) — UUID (v7) of the VeriFactu record. - `object` (string, required, enum: `verifactu_record`) - `type` (string, required) — Tipo de registro (`alta` / `anulacion`). - `invoice_type` (string, required) — AEAT invoice type (F1, F2, F3, R1-R5, …). - `invoice_number` (string, required) — Number of the associated invoice. - `date` (string, required) — Record date. - `amount` (number, required) — Total amount of the associated invoice. - `status` (string, required) — Transmission status (pending, submitted, accepted, rejected, error). - `huella` (string, required) — Chained SHA-256 fingerprint of the record (VeriFactu fingerprint). - `aeat_submission_id` (string | null, required) — Submission identifier assigned by AEAT (column `aeat_submission_id`). `null` until AEAT returns it. Never the internal PK of the record. - `aeat_csv` (string | null, required) — Secure Verification Code (Código Seguro de Verificación) returned by AEAT on acceptance. `null` until AEAT assigns it. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `transmitted_at` (string | null, required, format: date-time) — ISO 8601 date of the last transmission that reached AEAT. `null` in PENDING/REJECTED/ERROR states. - `is_simplificada` (boolean, required) — Whether the invoice is simplified (F2). - `is_substitute_for_simplified` (boolean, required) — Whether it substitutes one or more simplified invoices (F3). - `created_at` (string, required, format: date-time) - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/records/{record}/retry — Retry VeriFactu transmission - **Operation ID**: `public-api.v1.verifactu.records.retry` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.retry Requeues a failed VeriFactu record for transmission to AEAT. Conflict (409) if already accepted, 422 if retry limit exceeded. ## Path parameters - `record` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `id` (string, required) - `message` (string, required, const: `Transmisión encolada correctamente.`) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/records/{record} — Retrieve a VeriFactu record - **Operation ID**: `public-api.v1.verifactu.records.show` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.show Retrieve a VeriFactu record by its `id` (UUID v7). Returns 404 `verifactu_record_not_found` if the record does not exist or belongs to another company. ## Path parameters - `record` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuRecord), required) — A VeriFactu (Spanish AEAT SIF) record (alta/anulación) associated with an invoice. Includes the hash chain (`huella`), AEAT CSV and transmission status for reconciliation. - `id` (string, required) — UUID (v7) of the VeriFactu record. - `object` (string, required, enum: `verifactu_record`) - `type` (string, required) — Tipo de registro (`alta` / `anulacion`). - `invoice_type` (string, required) — AEAT invoice type (F1, F2, F3, R1-R5, …). - `invoice_number` (string, required) — Number of the associated invoice. - `date` (string, required) — Record date. - `amount` (number, required) — Total amount of the associated invoice. - `status` (string, required) — Transmission status (pending, submitted, accepted, rejected, error). - `huella` (string, required) — Chained SHA-256 fingerprint of the record (VeriFactu fingerprint). - `aeat_submission_id` (string | null, required) — Submission identifier assigned by AEAT (column `aeat_submission_id`). `null` until AEAT returns it. Never the internal PK of the record. - `aeat_csv` (string | null, required) — Secure Verification Code (Código Seguro de Verificación) returned by AEAT on acceptance. `null` until AEAT assigns it. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `transmitted_at` (string | null, required, format: date-time) — ISO 8601 date of the last transmission that reached AEAT. `null` in PENDING/REJECTED/ERROR states. - `is_simplificada` (boolean, required) — Whether the invoice is simplified (F2). - `is_substitute_for_simplified` (boolean, required) — Whether it substitutes one or more simplified invoices (F3). - `created_at` (string, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/verifactu/records/{record}/subsanar — Subsanar a rejected VeriFactu record - **Operation ID**: `public-api.v1.verifactu.records.subsanar` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.records.subsanar Correct (subsana) an AEAT-rejected VeriFactu record: regenerate the correctable content from the source invoice keeping the original `huella`, reset the transmission round and re-queue the AEAT transmission (202). Returns 422 `record_not_rejected` if the record is not rejected, or `requires_annulment` when the correction affects fingerprint fields (annul + new alta required instead). ## Path parameters - `record` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **202** - Body (`application/json`): - `data` (object, required) - `id` (string, required) - `message` (string, required, const: `Subsanación encolada. El registro se reenviará a la AEAT en unos segundos.`) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/verifactu/settings — Update VeriFactu settings - **Operation ID**: `public-api.v1.verifactu.settings.update` - **Tag**: VeriFactu - **Required scope**: `verifactu:write` — Create and update verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.settings.update Update the VeriFactu settings of your company (e.g. mode/environment). Returns 422 `business_rule_violation` when a transition is locked by AEAT compliance (for example, once VeriFactu mode has been enabled it cannot be silently disabled). ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 5 properties; none of them required. - `enabled` (boolean, optional) - `mode` (string, optional, enum: `verifactu`, `no_verifactu`) - `auto_transmit` (boolean, optional) - `environment` (string, optional, enum: `sandbox`, `production`) - `notification_emails` (array, optional, maxItems 5) ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuConfig), required) — VeriFactu configuration of your company (mode, environment, enrollment). Returned by `GET /v1/verifactu/config` and `PUT /v1/verifactu/settings`. The certificate password is never exposed. - `object` (string, required, enum: `verifactu_config`) - `enabled` (boolean, required) — Indicates whether VeriFactu is enabled for the company. - `mode` (string, required) — VeriFactu operation mode. - `auto_transmit` (boolean, required) — Indicates whether transmissions to AEAT are sent automatically. - `environment` (string, required) — Entorno AEAT (`sandbox` / `production`). - `notification_emails` (array, required) — Notification emails for VeriFactu events. - `is_locked_until` (string | null, required, format: date-time) — Date until which the mode change is locked, or `null` if not locked. - `has_active_certificate` (boolean, required) — Indica si hay un certificado activo configurado. - `active_certificate_id` (string | null, required) — UUID (v7) of the active certificate, or `null` if there is none. - `updated_at` (string | null, required, format: date-time) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/verifactu/stats — Get VeriFactu stats - **Operation ID**: `public-api.v1.verifactu.stats` - **Tag**: VeriFactu - **Required scope**: `verifactu:read` — Read verifactu. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/verifactu/public-api.v1.verifactu.stats Aggregated KPIs of your VeriFactu records: total count, counts per status (pending, submitted, accepted, rejected, error), breakdown by record and invoice type, and last transmission timestamp. Accepts optional `date_from`, `date_to`, and `environment` filters. Returned as `{ "data": VeriFactuStats }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (VeriFactuStats), required) — Resumen agregado de los registros VeriFactu de la empresa autenticada: conteos por estado y desglose por tipo. Devuelto por `GET /v1/verifactu/stats`. - `object` (string, required, enum: `verifactu_stats`) - `total_records` (integer, required) — Total de registros VeriFactu. - `pending` (integer, required) — Records pending transmission. - `submitted` (integer, required) — Records submitted to AEAT (without a final response yet). - `accepted` (integer, required) — Records accepted by AEAT. - `rejected` (integer, required) — Records rejected by AEAT. - `error` (integer, required) — Registros en estado de error. - `by_record_type` (object, required) — Count by record type (registration/cancellation). - `by_invoice_type` (object, required) — Count by AEAT invoice type (F1, F2, F3, …). - `last_transmission_at` (string | null, required, format: date-time) — Date of the last transmission to AEAT, or `null` if there is none. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/webhook_endpoints — Create a webhook endpoint - **Operation ID**: `public-api.v1.webhook_endpoints.create` - **Tag**: Webhooks - **Required scope**: `webhooks:write` — Create and update webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.create Create a webhook endpoint that receives event notifications via HTTPS callbacks. The signing `secret` is returned **once** in this response and never again — store it securely. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 8 properties; 2 required: `url`, `enabled_events`. - `url` (string, required, maxLength 2048, pattern: `^https:\/\/`) - `description` (string | null, optional, maxLength 255) - `api_version` (string | null, optional) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `timeout_seconds` (integer | null, optional) - `enabled_events` (array, required) - `ip_allowlist` (array | null, optional) - `custom_headers` (object (CustomHeaders), optional) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. ## Responses - **201** — Webhook endpoint created successfully. The `Location` header contains the canonical URL of the newly created resource. - Body (`application/json`): - `data` (object (WebhookEndpointWithSecret), required) — A WebhookEndpoint returned once at creation or after secret rotation. Includes the plain-text signing secret. - `id` (string, required) - `object` (string, required, enum: `webhook_endpoint`) - `url` (string, required, format: uri) — HTTPS callback URL. - `description` (string | null, required) - `enabled_events` (array, required) — List of event types this endpoint subscribes to. - `status` (string, required) — enabled, disabled, or paused. - `ip_allowlist` (array | null, required) — List of allowed source IPs. - `delivery_success_rate_24h` (number | null, required) - `last_delivery_at` (string | null, required, format: date-time) - `last_failure_at` (string | null, required, format: date-time) - `previous_secret_valid_until` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `api_version` (string | null, required) — API version (date-based, e.g. `2026-05-01`) pinned for the payloads delivered to this endpoint. `null` means the account default applies. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `custom_headers` (object (CustomHeaders), required) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. - `timeout_seconds` (integer | null, required) — HTTP request timeout in seconds applied when delivering to this endpoint (1–30). `null` means the system default applies. - `degraded_since` (string | null, required, format: date-time) — Timestamp since which the endpoint is considered degraded (sustained delivery failures). `null` when healthy. - `secret` (string, required) — Signing secret. Returned only at creation or after rotation. - **401** — Missing or invalid API key. - **402** — The operation requires a payment that could not be completed: either no payment method is on file (`error.details.payment_setup_url` links to the Billing Portal where it can be set up), the immediate charge was declined by the payment provider, or the account lacks the plan or add-on this operation bills against. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # DELETE /v1/webhook_endpoints/{webhook_endpoint} — Delete a webhook endpoint - **Operation ID**: `public-api.v1.webhook_endpoints.delete` - **Tag**: Webhooks - **Required scope**: `webhooks:delete` — Delete webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.delete Delete a webhook endpoint. In-flight deliveries are not cancelled but no new deliveries are queued. ## Path parameters - `webhook_endpoint` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/webhook_endpoints/{webhook_endpoint}/deliveries — List webhook deliveries - **Operation ID**: `public-api.v1.webhook_endpoints.deliveries.list` - **Tag**: Webhooks - **Required scope**: `webhooks:read` — Read webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.deliveries.list List delivery attempts for a webhook endpoint with cursor-based pagination. Each delivery captures the HTTP response status, body (truncated), duration, and retry schedule. ## Path parameters - `webhook_endpoint` (string, required) ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional) — Delivery status (pending, succeeded, failed). - `status[in]` (string, optional) — Delivery status (pending, succeeded, failed). - `event` (string, optional) — Type of the delivered event. - `event[in]` (string, optional) — Type of the delivered event. - `created[gte]` (string, optional, format: date-time) — Delivery creation date (ISO 8601). - `created[lte]` (string, optional, format: date-time) — Delivery creation date (ISO 8601). - `created[gt]` (string, optional, format: date-time) — Delivery creation date (ISO 8601). - `created[lt]` (string, optional, format: date-time) — Delivery creation date (ISO 8601). ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `webhook_delivery`) - `webhook_endpoint_id` (string, required) - `event_id` (string, required) - `event_name` (string, required) - `status` (string, required, enum: `pending`, `delivered`, `failed`, `failed_permanently`, `cancelled`) — Delivery status. `pending` (queued / in-flight), `delivered` (2xx received), `failed` (recoverable failure, will retry), `failed_permanently` (all attempts exhausted), `cancelled` (delivery aborted, terminal). - `attempt` (integer, required) - `next_retry_at` (string | null, required, format: date-time) - `response_status` (integer | null, required) — HTTP status from the endpoint. - `response_body_truncated` (string | null, required) - `duration_ms` (integer | null, required) - `signature` (string | null, required) — HMAC SHA-256 signature header value (`t=...,v1=...`) sent with this delivery, for debugging signature verification. `null` for deliveries created before signing or not yet attempted. - `request_headers` (object, required) — HTTP headers sent with this delivery attempt (debug). Empty object when no headers were captured. - `completed_at` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `payload` (object (WebhookEventPayloadInvoiceCreated) | object (WebhookEventPayloadInvoiceAutoCreated) | object (WebhookEventPayloadInvoiceCorrectiveAutoCreated) | object (WebhookEventPayloadInvoiceSubscriptionAutoCreated) | object (WebhookEventPayloadInvoiceUpdated) | object (WebhookEventPayloadInvoiceSent) | object (WebhookEventPayloadInvoicePaid) | object (WebhookEventPayloadInvoiceCancelled) | object (WebhookEventPayloadInvoiceAnnulled) | object (WebhookEventPayloadInvoiceOverdue) | object (WebhookEventPayloadInvoiceDeleted) | object (WebhookEventPayloadInvoiceNumberAssigned) | object (WebhookEventPayloadInvoiceRectified) | object (WebhookEventPayloadInvoiceEmailSent) | object (WebhookEventPayloadInvoiceEmailFailed) | object (WebhookEventPayloadInvoicePaymentReminderSent) | object (WebhookEventPayloadInvoiceSimplifiedCreated) | object (WebhookEventPayloadInvoiceSimplifiedSubstituted) | object (WebhookEventPayloadInvoiceSubstitutedByComplete) | object (WebhookEventPayloadInvoiceVerifactuSubmitted) | object (WebhookEventPayloadInvoiceVerifactuFailed) | object (WebhookEventPayloadInvoiceMetadataChanged) | object (WebhookEventPayloadQuoteCreated) | object (WebhookEventPayloadQuoteUpdated) | object (WebhookEventPayloadQuoteDeleted) | object (WebhookEventPayloadQuoteApproved) | object (WebhookEventPayloadQuoteRejected) | object (WebhookEventPayloadQuoteConverted) | object (WebhookEventPayloadQuoteExpired) | object (WebhookEventPayloadQuoteMarkedAsPending) | object (WebhookEventPayloadQuoteCancelled) | object (WebhookEventPayloadQuoteNumberAssigned) | object (WebhookEventPayloadQuoteMetadataChanged) | object (WebhookEventPayloadQuoteEmailSent) | object (WebhookEventPayloadQuoteEmailFailed) | object (WebhookEventPayloadProformaCreated) | object (WebhookEventPayloadProformaUpdated) | object (WebhookEventPayloadProformaDeleted) | object (WebhookEventPayloadProformaAccepted) | object (WebhookEventPayloadProformaRejected) | object (WebhookEventPayloadProformaCancelled) | object (WebhookEventPayloadProformaExpired) | object (WebhookEventPayloadProformaConvertedToInvoice) | object (WebhookEventPayloadProformaNumberAssigned) | object (WebhookEventPayloadProformaMetadataChanged) | object (WebhookEventPayloadProformaEmailSent) | object (WebhookEventPayloadProformaEmailFailed) | object (WebhookEventPayloadDeliveryNoteCreated) | object (WebhookEventPayloadDeliveryNoteUpdated) | object (WebhookEventPayloadDeliveryNoteStatusChanged) | object (WebhookEventPayloadDeliveryNoteSigned) | object (WebhookEventPayloadDeliveryNoteConverted) | object (WebhookEventPayloadDeliveryNoteEmailSent) | object (WebhookEventPayloadDeliveryNoteEmailFailed) | object (WebhookEventPayloadPurchaseInvoiceCreated) | object (WebhookEventPayloadPurchaseInvoiceUpdated) | object (WebhookEventPayloadPurchaseInvoicePaid) | object (WebhookEventPayloadPurchaseInvoiceCancelled) | object (WebhookEventPayloadPurchaseInvoiceMetadataChanged) | object (WebhookEventPayloadPurchaseInvoicePaymentRegistered) | object (WebhookEventPayloadRecurringInvoiceCreated) | object (WebhookEventPayloadRecurringInvoiceActivated) | object (WebhookEventPayloadRecurringInvoicePaused) | object (WebhookEventPayloadRecurringInvoiceUpdated) | object (WebhookEventPayloadRecurringInvoiceDeleted) | object (WebhookEventPayloadRecurringInvoiceCompleted) | object (WebhookEventPayloadRecurringInvoiceExecuted) | object (WebhookEventPayloadRecurringInvoiceFailed) | object (WebhookEventPayloadRecurringInvoiceMetadataChanged) | object (WebhookEventPayloadRecurringInvoiceCancelled) | object (WebhookEventPayloadClientCreated) | object (WebhookEventPayloadClientUpdated) | object (WebhookEventPayloadClientDeleted) | object (WebhookEventPayloadClientMetadataChanged) | object (WebhookEventPayloadProductCreated) | object (WebhookEventPayloadProductUpdated) | object (WebhookEventPayloadPaymentReceived) | object (WebhookEventPayloadTaxMetadataChanged) | object (WebhookEventPayloadTaxValidityChanged) | object (WebhookEventPayloadTaxExternalReferenceChanged) | object (WebhookEventPayloadSeriesCreated) | object (WebhookEventPayloadSeriesUpdated) | object (WebhookEventPayloadSeriesDeleted) | object (WebhookEventPayloadSeriesArchived) | object (WebhookEventPayloadSeriesUnarchived) | object (WebhookEventPayloadSeriesMarkedAsDefault) | object (WebhookEventPayloadSeriesDemotedFromDefault) | object (WebhookEventPayloadSeriesYearReset) | object (WebhookEventPayloadSeriesMonthReset) | object (WebhookEventPayloadSeriesNumberConsumed) | object (WebhookEventPayloadFacturaeFaceSubmitted) | object (WebhookEventPayloadFacturaeFaceStatusChanged) | object (WebhookEventPayloadFacturaeFaceCancellationRequested) | object (WebhookEventPayloadPayoutReconciled) | object (WebhookEventPayloadEmployeeCreated) | object (WebhookEventPayloadEmployeeUpdated) | object (WebhookEventPayloadEmployeeDeactivated) | object (WebhookEventPayloadEmployeeInvited) | object (WebhookEventPayloadTimeEntryRecorded) | object (WebhookEventPayloadTimeEntryCorrected) | object (WebhookEventPayloadAbsenceRequested) | object (WebhookEventPayloadAbsenceApproved) | object (WebhookEventPayloadAbsenceRejected) | object (WebhookEventPayloadMonthlyRegisterClosed), required) — The event envelope delivered to a webhook endpoint, discriminated by `type`. Each variant carries the typed `data` payload for its event type. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/webhook_endpoints/{webhook_endpoint}/deliveries/{delivery}/replay — Replay webhook delivery - **Operation ID**: `public-api.v1.webhook_endpoints.deliveries.replay` - **Tag**: Webhooks - **Required scope**: `webhooks:write` — Create and update webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.deliveries.replay Re-queue a webhook delivery. A new delivery attempt is created (with `attempt: 1`) for the same event/endpoint pair. ## Path parameters - `webhook_endpoint` (string, required) - `delivery` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **202** - Body (`application/json`): - `data` (object, required) - `new_delivery_id` (string, required) - `webhook_endpoint_id` (string, required) - `status` (string, required, const: `queued`) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/webhook_endpoints/{webhook_endpoint}/deliveries/{delivery} — Retrieve webhook delivery - **Operation ID**: `public-api.v1.webhook_endpoints.deliveries.show` - **Tag**: Webhooks - **Required scope**: `webhooks:read` — Read webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.deliveries.show Retrieve a single delivery attempt by its `uuid`, including the full event payload that was delivered. ## Path parameters - `webhook_endpoint` (string, required) - `delivery` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WebhookDelivery), required) — A single attempt to deliver an event to a webhook endpoint. - `id` (string, required) - `object` (string, required, enum: `webhook_delivery`) - `webhook_endpoint_id` (string, required) - `event_id` (string, required) - `event_name` (string, required) - `status` (string, required, enum: `pending`, `delivered`, `failed`, `failed_permanently`, `cancelled`) — Delivery status. `pending` (queued / in-flight), `delivered` (2xx received), `failed` (recoverable failure, will retry), `failed_permanently` (all attempts exhausted), `cancelled` (delivery aborted, terminal). - `attempt` (integer, required) - `next_retry_at` (string | null, required, format: date-time) - `response_status` (integer | null, required) — HTTP status from the endpoint. - `response_body_truncated` (string | null, required) - `duration_ms` (integer | null, required) - `signature` (string | null, required) — HMAC SHA-256 signature header value (`t=...,v1=...`) sent with this delivery, for debugging signature verification. `null` for deliveries created before signing or not yet attempted. - `request_headers` (object, required) — HTTP headers sent with this delivery attempt (debug). Empty object when no headers were captured. - `completed_at` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `payload` (object (WebhookEventPayloadInvoiceCreated) | object (WebhookEventPayloadInvoiceAutoCreated) | object (WebhookEventPayloadInvoiceCorrectiveAutoCreated) | object (WebhookEventPayloadInvoiceSubscriptionAutoCreated) | object (WebhookEventPayloadInvoiceUpdated) | object (WebhookEventPayloadInvoiceSent) | object (WebhookEventPayloadInvoicePaid) | object (WebhookEventPayloadInvoiceCancelled) | object (WebhookEventPayloadInvoiceAnnulled) | object (WebhookEventPayloadInvoiceOverdue) | object (WebhookEventPayloadInvoiceDeleted) | object (WebhookEventPayloadInvoiceNumberAssigned) | object (WebhookEventPayloadInvoiceRectified) | object (WebhookEventPayloadInvoiceEmailSent) | object (WebhookEventPayloadInvoiceEmailFailed) | object (WebhookEventPayloadInvoicePaymentReminderSent) | object (WebhookEventPayloadInvoiceSimplifiedCreated) | object (WebhookEventPayloadInvoiceSimplifiedSubstituted) | object (WebhookEventPayloadInvoiceSubstitutedByComplete) | object (WebhookEventPayloadInvoiceVerifactuSubmitted) | object (WebhookEventPayloadInvoiceVerifactuFailed) | object (WebhookEventPayloadInvoiceMetadataChanged) | object (WebhookEventPayloadQuoteCreated) | object (WebhookEventPayloadQuoteUpdated) | object (WebhookEventPayloadQuoteDeleted) | object (WebhookEventPayloadQuoteApproved) | object (WebhookEventPayloadQuoteRejected) | object (WebhookEventPayloadQuoteConverted) | object (WebhookEventPayloadQuoteExpired) | object (WebhookEventPayloadQuoteMarkedAsPending) | object (WebhookEventPayloadQuoteCancelled) | object (WebhookEventPayloadQuoteNumberAssigned) | object (WebhookEventPayloadQuoteMetadataChanged) | object (WebhookEventPayloadQuoteEmailSent) | object (WebhookEventPayloadQuoteEmailFailed) | object (WebhookEventPayloadProformaCreated) | object (WebhookEventPayloadProformaUpdated) | object (WebhookEventPayloadProformaDeleted) | object (WebhookEventPayloadProformaAccepted) | object (WebhookEventPayloadProformaRejected) | object (WebhookEventPayloadProformaCancelled) | object (WebhookEventPayloadProformaExpired) | object (WebhookEventPayloadProformaConvertedToInvoice) | object (WebhookEventPayloadProformaNumberAssigned) | object (WebhookEventPayloadProformaMetadataChanged) | object (WebhookEventPayloadProformaEmailSent) | object (WebhookEventPayloadProformaEmailFailed) | object (WebhookEventPayloadDeliveryNoteCreated) | object (WebhookEventPayloadDeliveryNoteUpdated) | object (WebhookEventPayloadDeliveryNoteStatusChanged) | object (WebhookEventPayloadDeliveryNoteSigned) | object (WebhookEventPayloadDeliveryNoteConverted) | object (WebhookEventPayloadDeliveryNoteEmailSent) | object (WebhookEventPayloadDeliveryNoteEmailFailed) | object (WebhookEventPayloadPurchaseInvoiceCreated) | object (WebhookEventPayloadPurchaseInvoiceUpdated) | object (WebhookEventPayloadPurchaseInvoicePaid) | object (WebhookEventPayloadPurchaseInvoiceCancelled) | object (WebhookEventPayloadPurchaseInvoiceMetadataChanged) | object (WebhookEventPayloadPurchaseInvoicePaymentRegistered) | object (WebhookEventPayloadRecurringInvoiceCreated) | object (WebhookEventPayloadRecurringInvoiceActivated) | object (WebhookEventPayloadRecurringInvoicePaused) | object (WebhookEventPayloadRecurringInvoiceUpdated) | object (WebhookEventPayloadRecurringInvoiceDeleted) | object (WebhookEventPayloadRecurringInvoiceCompleted) | object (WebhookEventPayloadRecurringInvoiceExecuted) | object (WebhookEventPayloadRecurringInvoiceFailed) | object (WebhookEventPayloadRecurringInvoiceMetadataChanged) | object (WebhookEventPayloadRecurringInvoiceCancelled) | object (WebhookEventPayloadClientCreated) | object (WebhookEventPayloadClientUpdated) | object (WebhookEventPayloadClientDeleted) | object (WebhookEventPayloadClientMetadataChanged) | object (WebhookEventPayloadProductCreated) | object (WebhookEventPayloadProductUpdated) | object (WebhookEventPayloadPaymentReceived) | object (WebhookEventPayloadTaxMetadataChanged) | object (WebhookEventPayloadTaxValidityChanged) | object (WebhookEventPayloadTaxExternalReferenceChanged) | object (WebhookEventPayloadSeriesCreated) | object (WebhookEventPayloadSeriesUpdated) | object (WebhookEventPayloadSeriesDeleted) | object (WebhookEventPayloadSeriesArchived) | object (WebhookEventPayloadSeriesUnarchived) | object (WebhookEventPayloadSeriesMarkedAsDefault) | object (WebhookEventPayloadSeriesDemotedFromDefault) | object (WebhookEventPayloadSeriesYearReset) | object (WebhookEventPayloadSeriesMonthReset) | object (WebhookEventPayloadSeriesNumberConsumed) | object (WebhookEventPayloadFacturaeFaceSubmitted) | object (WebhookEventPayloadFacturaeFaceStatusChanged) | object (WebhookEventPayloadFacturaeFaceCancellationRequested) | object (WebhookEventPayloadPayoutReconciled) | object (WebhookEventPayloadEmployeeCreated) | object (WebhookEventPayloadEmployeeUpdated) | object (WebhookEventPayloadEmployeeDeactivated) | object (WebhookEventPayloadEmployeeInvited) | object (WebhookEventPayloadTimeEntryRecorded) | object (WebhookEventPayloadTimeEntryCorrected) | object (WebhookEventPayloadAbsenceRequested) | object (WebhookEventPayloadAbsenceApproved) | object (WebhookEventPayloadAbsenceRejected) | object (WebhookEventPayloadMonthlyRegisterClosed), required) — The event envelope delivered to a webhook endpoint, discriminated by `type`. Each variant carries the typed `data` payload for its event type. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/webhook_endpoints — List all webhook endpoints - **Operation ID**: `public-api.v1.webhook_endpoints.list` - **Tag**: Webhooks - **Required scope**: `webhooks:read` — Read webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.list List your webhook endpoints with cursor-based pagination. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required) - `object` (string, required, enum: `webhook_endpoint`) - `url` (string, required, format: uri) — HTTPS callback URL. - `description` (string | null, required) - `enabled_events` (array, required) — List of event types this endpoint subscribes to. - `status` (string, required) — enabled, disabled, or paused. - `ip_allowlist` (array | null, required) — List of allowed source IPs. - `delivery_success_rate_24h` (number | null, required) - `last_delivery_at` (string | null, required, format: date-time) - `last_failure_at` (string | null, required, format: date-time) - `previous_secret_valid_until` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `api_version` (string | null, required) — API version (date-based, e.g. `2026-05-01`) pinned for the payloads delivered to this endpoint. `null` means the account default applies. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `custom_headers` (object (CustomHeaders), required) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. - `timeout_seconds` (integer | null, required) — HTTP request timeout in seconds applied when delivering to this endpoint (1–30). `null` means the system default applies. - `degraded_since` (string | null, required, format: date-time) — Timestamp since which the endpoint is considered degraded (sustained delivery failures). `null` when healthy. - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/webhook_endpoints/{webhook_endpoint}/ping — Ping webhook endpoint - **Operation ID**: `public-api.v1.webhook_endpoints.ping` - **Tag**: Webhooks - **Required scope**: `webhooks:write` — Create and update webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.ping Send a test event (`webhook.ping`) to the endpoint to verify it is reachable and the signature handshake works. The synthetic delivery appears in `GET /webhook_endpoints/{webhook_endpoint}/deliveries`. ## Path parameters - `webhook_endpoint` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `success` (boolean, required) - `http_status` (integer | null, required) - `response_body` (string | null, required) - `error_message` (string | null, required) - `duration_ms` (integer, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/webhook_endpoints/{webhook_endpoint}/rotate_secret — Rotate webhook secret - **Operation ID**: `public-api.v1.webhook_endpoints.rotate_secret` - **Tag**: Webhooks - **Required scope**: `webhooks:write` — Create and update webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.rotate_secret Rotate the signing secret of a webhook endpoint. The new secret is returned **once** in this response. The previous secret remains valid for a 24-hour grace period (see `previous_secret_valid_until`) to allow zero-downtime rotation. ## Path parameters - `webhook_endpoint` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WebhookEndpointWithSecret), required) — A WebhookEndpoint returned once at creation or after secret rotation. Includes the plain-text signing secret. - `id` (string, required) - `object` (string, required, enum: `webhook_endpoint`) - `url` (string, required, format: uri) — HTTPS callback URL. - `description` (string | null, required) - `enabled_events` (array, required) — List of event types this endpoint subscribes to. - `status` (string, required) — enabled, disabled, or paused. - `ip_allowlist` (array | null, required) — List of allowed source IPs. - `delivery_success_rate_24h` (number | null, required) - `last_delivery_at` (string | null, required, format: date-time) - `last_failure_at` (string | null, required, format: date-time) - `previous_secret_valid_until` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `api_version` (string | null, required) — API version (date-based, e.g. `2026-05-01`) pinned for the payloads delivered to this endpoint. `null` means the account default applies. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `custom_headers` (object (CustomHeaders), required) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. - `timeout_seconds` (integer | null, required) — HTTP request timeout in seconds applied when delivering to this endpoint (1–30). `null` means the system default applies. - `degraded_since` (string | null, required, format: date-time) — Timestamp since which the endpoint is considered degraded (sustained delivery failures). `null` when healthy. - `secret` (string, required) — Signing secret. Returned only at creation or after rotation. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/webhook_endpoints/{webhook_endpoint} — Retrieve a webhook endpoint - **Operation ID**: `public-api.v1.webhook_endpoints.show` - **Tag**: Webhooks - **Required scope**: `webhooks:read` — Read webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.show Retrieve a webhook endpoint by its `uuid`. The signing secret is never exposed in this representation. ## Path parameters - `webhook_endpoint` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WebhookEndpoint), required) — An HTTPS endpoint that receives webhook events. The secret is never exposed in this representation. - `id` (string, required) - `object` (string, required, enum: `webhook_endpoint`) - `url` (string, required, format: uri) — HTTPS callback URL. - `description` (string | null, required) - `enabled_events` (array, required) — List of event types this endpoint subscribes to. - `status` (string, required) — enabled, disabled, or paused. - `ip_allowlist` (array | null, required) — List of allowed source IPs. - `delivery_success_rate_24h` (number | null, required) - `last_delivery_at` (string | null, required, format: date-time) - `last_failure_at` (string | null, required, format: date-time) - `previous_secret_valid_until` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `api_version` (string | null, required) — API version (date-based, e.g. `2026-05-01`) pinned for the payloads delivered to this endpoint. `null` means the account default applies. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `custom_headers` (object (CustomHeaders), required) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. - `timeout_seconds` (integer | null, required) — HTTP request timeout in seconds applied when delivering to this endpoint (1–30). `null` means the system default applies. - `degraded_since` (string | null, required, format: date-time) — Timestamp since which the endpoint is considered degraded (sustained delivery failures). `null` when healthy. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/webhook_endpoints/{webhook_endpoint}/test_event — Send a test event - **Operation ID**: `public-api.v1.webhook_endpoints.test_event` - **Tag**: Webhooks - **Required scope**: `webhooks:write` — Create and update webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.test_event Trigger a test delivery of a real catalog event type to this endpoint, marked `test: true` in the delivered envelope. Unlike `ping` (a synthetic `webhook.ping`), this records a real `Event` (visible in `GET /events`) and queues a signed, retried `WebhookDelivery`. Optionally pass `type` to choose which subscribed event to simulate. The delivery reaches only this endpoint. ## Path parameters - `webhook_endpoint` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 1 property; none of them required. Trigger a test delivery to the webhook endpoint. `type` is optional: when omitted the endpoint first subscribed event is used; when set it must belong to the closed event catalog and be one of the endpoint subscribed events (otherwise 422). - `type` (string | null, optional, enum: `invoice.created`, `invoice.auto_created`, `invoice.corrective_auto_created`, `invoice.subscription_auto_created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.cancelled`, `invoice.annulled`, `invoice.overdue`, `invoice.deleted`, `invoice.number_assigned`, `invoice.rectified`, `invoice.email_sent`, `invoice.email_failed`, `invoice.payment_reminder_sent`, `invoice.simplified_created`, `invoice.simplified_substituted`, `invoice.substituted_by_complete`, `invoice.verifactu_submitted`, `invoice.verifactu_failed`, `invoice.metadata_changed`, `quote.created`, `quote.updated`, `quote.deleted`, `quote.approved`, `quote.rejected`, `quote.converted`, `quote.expired`, `quote.marked_as_pending`, `quote.cancelled`, `quote.number_assigned`, `quote.metadata_changed`, `quote.email_sent`, `quote.email_failed`, `proforma.created`, `proforma.updated`, `proforma.deleted`, `proforma.accepted`, `proforma.rejected`, `proforma.cancelled`, `proforma.expired`, `proforma.converted_to_invoice`, `proforma.number_assigned`, `proforma.metadata_changed`, `proforma.email_sent`, `proforma.email_failed`, `delivery_note.created`, `delivery_note.updated`, `delivery_note.status_changed`, `delivery_note.signed`, `delivery_note.converted`, `delivery_note.email_sent`, `delivery_note.email_failed`, `purchase_invoice.created`, `purchase_invoice.updated`, `purchase_invoice.paid`, `purchase_invoice.cancelled`, `purchase_invoice.metadata_changed`, `purchase_invoice.payment_registered`, `recurring_invoice.created`, `recurring_invoice.activated`, `recurring_invoice.paused`, `recurring_invoice.updated`, `recurring_invoice.deleted`, `recurring_invoice.completed`, `recurring_invoice.executed`, `recurring_invoice.failed`, `recurring_invoice.metadata_changed`, `recurring_invoice.cancelled`, `client.created`, `client.updated`, `client.deleted`, `client.metadata_changed`, `product.created`, `product.updated`, `payment.received`, `tax.metadata_changed`, `tax.validity_changed`, `tax.external_reference_changed`, `series.created`, `series.updated`, `series.deleted`, `series.archived`, `series.unarchived`, `series.marked_as_default`, `series.demoted_from_default`, `series.year_reset`, `series.month_reset`, `series.number_consumed`, `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested`, `payout.reconciled`, `mandate.activated`, `mandate.cancelled`, `mandate.expired`, `employee.created`, `employee.updated`, `employee.deactivated`, `employee.invited`, `time_entry.recorded`, `time_entry.corrected`, `absence.requested`, `absence.approved`, `absence.rejected`, `monthly_register.closed`) ## Responses - **202** - Body (`application/json`): - `data` (object, required) - `object` (string, required, const: `webhook_test_event`) - `test` (boolean, required) - `type` (string | null, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/webhook_endpoints/{webhook_endpoint} — Update a webhook endpoint - **Operation ID**: `public-api.v1.webhook_endpoints.update` - **Tag**: Webhooks - **Required scope**: `webhooks:write` — Create and update webhooks. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/webhooks/public-api.v1.webhook_endpoints.update Update a webhook endpoint (URL, description, enabled events, status, IP allowlist). ## Path parameters - `webhook_endpoint` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, optional. 8 properties; none of them required. - `url` (string | null, optional, maxLength 2048, pattern: `^https:\/\/`) - `description` (string | null, optional, maxLength 255) - `api_version` (string | null, optional) - `metadata` (object (Metadata) | null, optional) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `timeout_seconds` (integer | null, optional) - `enabled_events` (array, optional) - `ip_allowlist` (array | null, optional) - `custom_headers` (object (CustomHeaders), optional) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. ## Responses - **200** - Body (`application/json`): - `data` (object (WebhookEndpoint), required) — An HTTPS endpoint that receives webhook events. The secret is never exposed in this representation. - `id` (string, required) - `object` (string, required, enum: `webhook_endpoint`) - `url` (string, required, format: uri) — HTTPS callback URL. - `description` (string | null, required) - `enabled_events` (array, required) — List of event types this endpoint subscribes to. - `status` (string, required) — enabled, disabled, or paused. - `ip_allowlist` (array | null, required) — List of allowed source IPs. - `delivery_success_rate_24h` (number | null, required) - `last_delivery_at` (string | null, required, format: date-time) - `last_failure_at` (string | null, required, format: date-time) - `previous_secret_valid_until` (string | null, required, format: date-time) - `created_at` (string, required, format: date-time) - `updated_at` (string, required, format: date-time) - `api_version` (string | null, required) — API version (date-based, e.g. `2026-05-01`) pinned for the payloads delivered to this endpoint. `null` means the account default applies. - `metadata` (object (Metadata) | null, required) — A free map of up to 50 key→value pairs for storing arbitrary structured data (values are strings up to 500 characters). Unlike `custom_fields` — an ordered list of typed `{field, value}` pairs with display semantics, present on the six document resources — `metadata` is an unordered map for opaque integration data; a document may carry both. The master resources (Client, Supplier) have no `custom_fields`, so their `metadata` doubles as the custom-fields store. **Reserved keys (read-only).** When the system auto-issues an invoice from a payment correlation (Stripe/GoCardless/MONEI), it writes `stripe_subscription_id`, `stripe_invoice_id`, `billing_reason`, `period_start` and `period_end` into that invoice metadata automatically. Do not set or overwrite them by hand — the platform owns them and a manual value may be replaced when the correlation runs. - `custom_headers` (object (CustomHeaders), required) — Custom HTTP headers (string → string) added to every delivery request for a webhook endpoint. Max 20 entries, each value ≤ 1024 chars. Header names are normalized to Title-Case. Reserved names (`Host`, `Content-Type`, `Factuarea-Signature`, `X-Forwarded-*`, …) are rejected. Empty object when none are set. Never put secrets here — values are returned verbatim by the API. - `timeout_seconds` (integer | null, required) — HTTP request timeout in seconds applied when delivering to this endpoint (1–30). `null` means the system default applies. - `degraded_since` (string | null, required, format: date-time) — Timestamp since which the endpoint is considered degraded (sustained delivery failures). `null` when healthy. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/work-schedules/{schedule}/archive — Archive a work schedule - **Operation ID**: `public-api.v1.work_schedules.archive` - **Tag**: Work Schedules - **Required scope**: `work_schedules:write` — Create and update work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.archive Archive a work schedule (transition `active` → `archived`), retiring it from use while preserving it. No request body. Returns 422 if it is already archived. Reversible via unarchive. ## Path parameters - `schedule` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WeeklySchedule), required) — A fixed weekly work schedule for the Control Horario (time tracking) module. Defines, per weekday, the expected time ranges (from which the expected hours and the planned start time are derived). Its `mode` decides how balances are computed downstream: `validated` (the period is assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `id` (string, required, format: uuid) — Opaque identifier of the work schedule, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `work_schedule`) — Always `work_schedule`. - `name` (string, required) — Human-readable name of the schedule (e.g. `Jornada partida`). - `mode` (string, required, enum: `validated`, `real_clocking`) — How the schedule computes balances: `validated` (period assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the sum of all time ranges in the pattern. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (assignable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/work-schedules/{schedule}/assign — Assign a schedule to an employee - **Operation ID**: `public-api.v1.work_schedules.assign` - **Tag**: Work Schedules - **Required scope**: `work_schedules:write` — Create and update work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.assign Assign the work schedule to an employee with an effective start date. `employee_id` (UUID v7, must belong to your company) and `effective_from` (`Y-m-d`) are required. Assigning closes the employee’s previously open assignment and opens the new one (an employee has at most one open assignment; history is preserved). An unknown employee returns 422 `assigned_employee_not_found`; an unknown schedule returns 404. Returns the created assignment. ## Path parameters - `schedule` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 2 required: `employee_id`, `effective_from`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) the schedule is assigned to. - `effective_from` (string, required, format: date) — Assignment effective start date, in `Y-m-d`. ## Responses - **201** - Body (`application/json`): - `data` (object (ScheduleAssignment), required) — The effective-dated assignment of a work schedule to an employee. Assigning a schedule closes the employee’s previously open assignment and opens a new one, preserving history. `effective_to` is `null` while the assignment is still open. - `id` (string, required, format: uuid) — Opaque identifier of the assignment, a UUID v7. - `object` (string, required, enum: `work_schedule_assignment`) — Always `work_schedule_assignment`. - `work_schedule_id` (string, required, format: uuid) — UUID v7 of the assigned work schedule. - `employee_id` (string, required, format: uuid) — UUID v7 of the assigned employee. - `effective_from` (string, required, format: date) — Date the assignment starts being effective, in `Y-m-d`. - `effective_to` (string | null, required, format: date) — Date the assignment stops being effective, in `Y-m-d`; `null` while open. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/work-schedules/{schedule}/assignments — List a schedule’s assignments - **Operation ID**: `public-api.v1.work_schedules.assignments` - **Tag**: Work Schedules - **Required scope**: `work_schedules:read` — Read work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.assignments List the employees with an open assignment (`effective_to` = null) to this work schedule, as a flat list under `{ "data": [ … ] }`. ## Path parameters - `schedule` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the assignment, a UUID v7. - `object` (string, required, enum: `work_schedule_assignment`) — Always `work_schedule_assignment`. - `work_schedule_id` (string, required, format: uuid) — UUID v7 of the assigned work schedule. - `employee_id` (string, required, format: uuid) — UUID v7 of the assigned employee. - `effective_from` (string, required, format: date) — Date the assignment starts being effective, in `Y-m-d`. - `effective_to` (string | null, required, format: date) — Date the assignment stops being effective, in `Y-m-d`; `null` while open. - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/work-schedules — Create a work schedule - **Operation ID**: `public-api.v1.work_schedules.create` - **Tag**: Work Schedules - **Required scope**: `work_schedules:write` — Create and update work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.create Create a weekly work schedule for the authenticated company (resolved from the API key, never from the payload). `name` and `week_pattern` are required; `mode` defaults to `validated`. The `week_pattern` is a list of weekdays (ISO 8601 1..7) each with its ordered, non-overlapping `HH:MM` time ranges (an empty `ranges` means a rest day). Returns the created schedule with its generated `id` (UUID v7); `weekly_hours` is derived from the pattern. ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 3 required: `name`, `mode`, `week_pattern`. - `name` (string, required, maxLength 120) — Weekly schedule name. - `mode` (string, required, enum: `validated`, `real_clocking`) — Schedule mode: `validated` (assumed fulfilled) or `real_clocking` (computed from real clock entries). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: list of days with their time ranges. - `day` (integer, required, min 1, max 7) — Day of the week in ISO 8601 format (1 Monday .. 7 Sunday). - `ranges` (array, required) — Time ranges for the day (empty = rest day). - `start` (string, required, pattern: `^([01]\d|2[0-3]):[0-5]\d$`) — Range start time (HH:MM, 00:00-23:59). - `end` (string, required, pattern: `^(?:([01]\d|2[0-3]):[0-5]\d|24:00)$`) — Range end time (HH:MM; 24:00 is allowed as end of workday). - `flexible` (boolean, optional) - `required_minutes` (integer | null, optional, min 1) ## Responses - **201** - Body (`application/json`): - `data` (object (WeeklySchedule), required) — A fixed weekly work schedule for the Control Horario (time tracking) module. Defines, per weekday, the expected time ranges (from which the expected hours and the planned start time are derived). Its `mode` decides how balances are computed downstream: `validated` (the period is assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `id` (string, required, format: uuid) — Opaque identifier of the work schedule, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `work_schedule`) — Always `work_schedule`. - `name` (string, required) — Human-readable name of the schedule (e.g. `Jornada partida`). - `mode` (string, required, enum: `validated`, `real_clocking`) — How the schedule computes balances: `validated` (period assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the sum of all time ranges in the pattern. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (assignable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/work-schedules/employee/{employee} — Get an employee’s current schedule - **Operation ID**: `public-api.v1.work_schedules.employee_schedule` - **Tag**: Work Schedules - **Required scope**: `work_schedules:read` — Read work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.employee_schedule Resolve the work schedule currently in effect (today) for an employee by its `id` (UUID v7). Returns 404 `schedule_assignment_not_found` when the employee has no schedule in effect (or belongs to another company). The result is the resolved schedule (`id` = UUID v7 of the schedule), not the assignment. ## Path parameters - `employee` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (EmployeeSchedule), required) — The work schedule currently in effect for an employee on a given date, resolved from the employee’s open assignment. `id` is the UUID v7 of the underlying work schedule (not of the assignment). - `id` (string, required, format: uuid) — UUID v7 of the work schedule currently in effect for the employee. - `object` (string, required, enum: `employee_work_schedule`) — Always `employee_work_schedule`. - `name` (string, required) — Human-readable name of the schedule. - `mode` (string, required, enum: `validated`, `real_clocking`) — Mode of the schedule: `validated` or `real_clocking`. - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the pattern. - `effective_from` (string, required, format: date) — Date the schedule became effective for the employee, in `Y-m-d`. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/work-schedules — List all work schedules - **Operation ID**: `public-api.v1.work_schedules.list` - **Tag**: Work Schedules - **Required scope**: `work_schedules:read` — Read work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.list List the weekly work schedules of your company with cursor-based pagination. Supports filtering by `status` (`active`/`archived`) and `mode` (`validated`/`real_clocking`), plus free-text `search` over the schedule name. ## Query parameters - `limit` (integer, optional, min 1, max 100, default: `25`) — Number of objects to return. - `starting_after` (string, optional, format: uuid) — Cursor for forward pagination. - `ending_before` (string, optional, format: uuid) — Cursor for backward pagination. - `status` (string, optional, enum: `active`, `archived`) — Lifecycle status of the work schedule. - `status[in]` (string, optional) — Lifecycle status of the work schedule. - `mode` (string, optional, enum: `validated`, `real_clocking`) — Time-tracking mode of the work schedule. - `mode[in]` (string, optional) — Time-tracking mode of the work schedule. - `search` (string, optional, maxLength 80) — Free-text search. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (array, required) - `id` (string, required, format: uuid) — Opaque identifier of the work schedule, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `work_schedule`) — Always `work_schedule`. - `name` (string, required) — Human-readable name of the schedule (e.g. `Jornada partida`). - `mode` (string, required, enum: `validated`, `real_clocking`) — How the schedule computes balances: `validated` (period assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the sum of all time ranges in the pattern. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (assignable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - `has_more` (boolean, required) — Whether more pages are available after this one. - `next_cursor` (string | null, required) — Opaque cursor for the next page, or `null` when `has_more` is `false`. The example below shows the shape used by most listings; others return a different one, so pass the value back verbatim instead of validating it. - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/work-schedules/{schedule} — Retrieve a work schedule - **Operation ID**: `public-api.v1.work_schedules.show` - **Tag**: Work Schedules - **Required scope**: `work_schedules:read` — Read work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.show Retrieve a single work schedule by its `id` (UUID v7). A schedule belonging to another company returns 404 `work_schedule_not_found` (anti-enumeration). ## Path parameters - `schedule` (string, required) ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WeeklySchedule), required) — A fixed weekly work schedule for the Control Horario (time tracking) module. Defines, per weekday, the expected time ranges (from which the expected hours and the planned start time are derived). Its `mode` decides how balances are computed downstream: `validated` (the period is assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `id` (string, required, format: uuid) — Opaque identifier of the work schedule, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `work_schedule`) — Always `work_schedule`. - `name` (string, required) — Human-readable name of the schedule (e.g. `Jornada partida`). - `mode` (string, required, enum: `validated`, `real_clocking`) — How the schedule computes balances: `validated` (period assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the sum of all time ranges in the pattern. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (assignable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # GET /v1/work-schedules/stats — Get work schedule stats - **Operation ID**: `public-api.v1.work_schedules.stats` - **Tag**: Work Schedules - **Required scope**: `work_schedules:read` — Read work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.stats Aggregated KPIs for your work schedules: total count, active and archived counts, a breakdown by mode (`validated`/`real_clocking`) and the number of employees with an assigned schedule. Returned as `{ "data": … }`. ## Request headers - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object, required) - `total` (integer, required) - `active` (integer, required) - `archived` (integer, required) - `validated` (integer, required) - `real_clocking` (integer, required) - `assigned_employees` (integer, required) - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/work-schedules/{schedule}/unarchive — Unarchive a work schedule - **Operation ID**: `public-api.v1.work_schedules.unarchive` - **Tag**: Work Schedules - **Required scope**: `work_schedules:write` — Create and update work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.unarchive Unarchive a work schedule (transition `archived` → `active`), returning it to use. No request body. Returns 422 if it is already active. ## Path parameters - `schedule` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Responses - **200** - Body (`application/json`): - `data` (object (WeeklySchedule), required) — A fixed weekly work schedule for the Control Horario (time tracking) module. Defines, per weekday, the expected time ranges (from which the expected hours and the planned start time are derived). Its `mode` decides how balances are computed downstream: `validated` (the period is assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `id` (string, required, format: uuid) — Opaque identifier of the work schedule, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `work_schedule`) — Always `work_schedule`. - `name` (string, required) — Human-readable name of the schedule (e.g. `Jornada partida`). - `mode` (string, required, enum: `validated`, `real_clocking`) — How the schedule computes balances: `validated` (period assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the sum of all time ranges in the pattern. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (assignable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # POST /v1/work-schedules/{schedule}/unassign — Unassign a schedule from an employee - **Operation ID**: `public-api.v1.work_schedules.unassign` - **Tag**: Work Schedules - **Required scope**: `work_schedules:write` — Create and update work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.unassign Close the employee’s open assignment to this schedule. `employee_id` (UUID v7) is required; `effective_to` (`Y-m-d`) is optional and defaults to today. Returns 404 when there is no open assignment. Responds 204 No Content. ## Path parameters - `schedule` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 2 properties; 1 required: `employee_id`. - `employee_id` (string, required, format: uuid) — Employee ID (UUID v7) whose assignment is removed. - `effective_to` (string | null, optional, format: date) — Assignment effective end date (Y-m-d); defaults to today when omitted. ## Responses - **204** — No content - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # PUT /v1/work-schedules/{schedule} — Update a work schedule - **Operation ID**: `public-api.v1.work_schedules.update` - **Tag**: Work Schedules - **Required scope**: `work_schedules:write` — Create and update work schedules. - **Authentication**: `Authorization: Bearer `, `X-API-Key: ` or an OAuth 2.1 access token. - **Docs**: https://docs.factuarea.com/api-reference/work-schedules/public-api.v1.work_schedules.update Fully replace a work schedule: `name`, `mode` and the complete `week_pattern` are required (there is no partial update of the pattern). `weekly_hours` is recomputed from the new pattern. Returns the updated schedule. ## Path parameters - `schedule` (string, required) ## Request headers - `Idempotency-Key` (string, optional, maxLength 255, minLength 1) — Client-generated opaque key (up to 255 characters; UUID v7 recommended) that makes retries safe: the first response is cached and replayed for repeats without re-executing the mutation. - `Factuarea-Version` (string, optional, format: date) — Pin the API version (`YYYY-MM-DD`, Stripe-style date versioning) for this request; omit to use the key's pinned version, or the latest if none. - `X-Active-Profile` (string, optional, format: uuid) — Operate on behalf of a child company (gestoría master key): pass its public `id` (UUID v7) and the request runs against that child's data without changing the key's scope, tier or environment (omit to use the key's own company). ## Request body `application/json`, required. 3 properties; 3 required: `name`, `mode`, `week_pattern`. - `name` (string, required, maxLength 120) — Weekly schedule name. - `mode` (string, required, enum: `validated`, `real_clocking`) — Schedule mode: `validated` (assumed fulfilled) or `real_clocking` (computed from real clock entries). - `week_pattern` (array, required, maxItems 7) — Full weekly pattern: list of days with their time ranges. - `day` (integer, required, min 1, max 7) — Day of the week in ISO 8601 format (1 Monday .. 7 Sunday). - `ranges` (array, required) — Time ranges for the day (empty = rest day). - `start` (string, required, pattern: `^([01]\d|2[0-3]):[0-5]\d$`) — Range start time (HH:MM, 00:00-23:59). - `end` (string, required, pattern: `^(?:([01]\d|2[0-3]):[0-5]\d|24:00)$`) — Range end time (HH:MM; 24:00 is allowed as end of workday). - `flexible` (boolean, optional) - `required_minutes` (integer | null, optional, min 1) ## Responses - **200** - Body (`application/json`): - `data` (object (WeeklySchedule), required) — A fixed weekly work schedule for the Control Horario (time tracking) module. Defines, per weekday, the expected time ranges (from which the expected hours and the planned start time are derived). Its `mode` decides how balances are computed downstream: `validated` (the period is assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `id` (string, required, format: uuid) — Opaque identifier of the work schedule, a UUID v7 (e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a9b`). - `object` (string, required, enum: `work_schedule`) — Always `work_schedule`. - `name` (string, required) — Human-readable name of the schedule (e.g. `Jornada partida`). - `mode` (string, required, enum: `validated`, `real_clocking`) — How the schedule computes balances: `validated` (period assumed fulfilled unless an exception) or `real_clocking` (only real clock-ins count). - `week_pattern` (array, required, maxItems 7) — Weekly pattern: one entry per configured weekday with its ordered, non-overlapping time ranges. A weekday absent from the list (or with an empty `ranges`) is a rest day. - `weekly_hours` (number, required, format: float) — Total weekly hours derived from the sum of all time ranges in the pattern. - `status` (string, required, enum: `active`, `archived`) — Lifecycle status: `active` (assignable) or `archived` (retired from use). - `created_at` (string | null, required, format: date-time) — Creation timestamp (ISO 8601). - `updated_at` (string | null, required, format: date-time) — Last-update timestamp (ISO 8601). - **401** — Missing or invalid API key. - **403** — The API key lacks the required scope for this operation. - **404** — The requested resource does not exist or belongs to another company. - **409** — The request conflicts with the current resource state — e.g. an idempotency key was reused with a different body, or the resource is in a state that does not allow this operation. - **422** — Validation failed. - **429** — Rate limit exceeded. - **500** — Unexpected server error. --- # API de Factuarea (/ca) L'API REST de Factuarea exposa recursos de facturació (clients, productes, factures, pressupostos, factures proforma, albarans, factures recurrents, factures de compra) sobre HTTPS amb autenticació per **API key**. Tota la superfície pública viu a [`https://api.factuarea.com/v1`](https://api.factuarea.com/v1) i retorna JSON. Cada recurs s'identifica per un `id` opac (un string UUID v7). Una seqüència de copiar i enganxar contra una clau `fact_test_`: verifica la teva clau, obtén una sèrie i un impost, crea un client, emet una factura i envia-la. ## Inici ràpid [#inici-ràpid] **L'API ve amb el teu pla** L'API pública està **inclosa en tots els plans de Factuarea** — sense programa beta ni add-on a banda. Durant el trial de 10 dies ja tens accés a l'API amb el tier `free`; els plans de pagament pugen el tier de rate limit. Consulta [Límits de peticions](/guides/rate-limits). **Crea la teva primera API key** Obre [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) i crea una clau amb els scopes que necessitis (per exemple `invoices:read,clients:read` per començar). Copia el secret **només un cop** — no el podràs tornar a veure. Tria l'entorn **Test** per obtenir una clau `fact_test_` que opera sobre un sandbox aïllat sense efectes en el món real. Crea contra ell primer i després crea una clau `fact_live_` per passar a producció. Consulta [Mode de prova i sandbox](/guides/test-mode). **Verifica la teva clau** Abans de res, confirma que la clau funciona. `GET /v1/account` introspecciona la credencial — retorna l'empresa a què pertany, el pla, i els **scopes** i el **tier** de límit de peticions de la mateixa clau (necessita `account:read`): ```bash curl https://api.factuarea.com/v1/account \ -H "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` ✅ Hauries de veure un `200` amb una instantània d'`account`: ```json { "data": { "object": "account", "company": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Acme Soluciones SL", "tax_id": "B12345678" }, "plan": { "slug": "empresario", "name": "Empresario" }, "api_key": { "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "name": "Sandbox integration", "prefix": "fact_test_3pXnR2Vb", "scopes": ["account:read", "clients:read", "invoices:read"], "tier": "starter" } } } ``` Si obtens `401 invalid_api_key`, torna a comprovar el valor. L'array `scopes` et diu exactament què pot fer aquesta clau — una crida posterior que falli amb `403 insufficient_scope` no en té algun. **Fes la teva primera petició de dades** Ara llista un recurs real. `GET /v1/clients` retorna un embolcall estàndard amb `data` (resultats), `has_more` i `next_cursor` ([paginació per cursor](/guides/pagination)): ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Llest per emetre la teva primera factura de principi a fi? Segueix l' [Inici ràpid](/guides/quickstart). Si reps un error, cerca'l a [Errors](/guides/errors) pel `code` retornat. **Configura webhooks (opcional)** Si la teva integració necessita reaccionar a esdeveniments (factura pagada, pressupost acceptat, etc.), configura un webhook endpoint signat amb HMAC SHA256. Consulta [Webhooks](/guides/webhooks). ## Què cobreix l'API [#què-cobreix-lapi] CRUD complet, cerca per tax ID, validació VIES. Productes amb preus, stock, SKU i tipus impositius. Factures, pressupostos, factures proforma, albarans, factures recurrents — amb línies, retencions i recàrrec d'equivalència. Enviar per email, marcar com a pagada/acceptada, generar PDF, anul·lar, crear factura rectificativa, convertir entre tipus. Factures de proveïdor amb pujada de PDF, mark\_paid, mark\_received. Sèries de numeració legal per tipus de document (de només lectura via API per garantir la continuïtat fiscal). Descàrrega de l'XML FacturaE 3.2.2 i enviaments a FACe — envia, segueix l'estat de tramitació i sol·licita anul·lacions. Empleats, horaris de treball, el registre de fitxatges, tancaments mensuals, absències, presència i festius — el registre de jornada del RD-ley 8/2019. Tota l'API com a eines Model Context Protocol, amb OAuth 2.1 i autenticació per API key — connecta Claude i altres agents en segons. ## Disseny del contracte [#disseny-del-contracte] L'API segueix els patrons que esperaries d'un proveïdor modern: * **Identificadors opacs** — la clau `id` porta un string UUID v7 en lloc d'un enter incremental. Consulta [Paginació](/guides/pagination) per a la semàntica del cursor. * **Errors normalitzats** — cada error retorna un embolcall amb `type`, `code`, `message`, `param`, `doc_url` i `request_id`. Consulta [Errors](/guides/errors). * **Idempotency keys** — suportades a cada `POST` per evitar duplicats en els reintents. Consulta [Idempotència](/guides/idempotency). * **Límits de peticions per tier** — quotes per minut i mensuals, amb capçaleres `X-RateLimit-*` a cada resposta. Consulta [Límits de peticions](/guides/rate-limits). * **Versionat per URL** — `/v1/*`. Els canvis incompatibles disparen `/v2/*` amb una política de deprecació documentada. Consulta [Versionat](/guides/versioning). * **Webhooks amb rotació de doble secret** — HMAC SHA256, reintent exponencial amb fins a 8 intents. Consulta [Webhooks](/guides/webhooks). ## SDKs [#sdks] Oferim [SDKs oficials de TypeScript i PHP](/sdks) (`@factuarea/sdk` i `factuarea/factuarea-php`) amb reintents, idempotència, paginació per cursor, errors tipats i verificació de webhooks integrats. Si el teu llenguatge no està cobert, qualsevol client HTTP estàndard (curl, Postman, axios, requests, Guzzle) funciona — l'API és REST pla sobre JSON. L'API REST pública complementa el client web de Factuarea ([`app.factuarea.com`](https://app.factuarea.com)) — no el reemplaça. Les operacions que l'API no exposa (gestió de plans, branding, configuració fiscal global de l'empresa) segueixen vivint a l'app. --- # Launch (/ca/changelog/launch) ## Control horari — 2026-07-11 [#control-horari--2026-07-11] Factuarea ja cobreix el deure de l'empresari espanyol de portar un registre diari de jornada — **RD-ley 8/2019**, art. 34.9 de l'Estatut dels Treballadors — i exposa tot el sistema de personal sobre el mateix contracte v1. És el **VeriFactu del control horari**: un ledger de sola addició segellat per una cadena de hash SHA-256 per empresa, on res no s'edita ni s'esborra i qualsevol manipulació trenca la cadena. Tota la superfície està protegida pel nou **mòdul `control_horario`**. Comença pel [resum de control horari](/guides/workforce-overview). * **Vuit dominis nous** — empleats (amb invitacions i facturació per assentament), horaris de treball, fitxatges (entrada/sortida, pauses, fitxatges retroactius i correccions), tancaments mensuals del registre, exportacions per a nòmines, absències (tipus, polítiques, sol·licituds, saldos i calendari), presència i festius. * **Scopes nous** — un conjunt dedicat dins del catàleg tancat: `employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read` i `payroll_exports:read`, tots darrere el mòdul `control_horario`. Consulta [Scopes i irreversibilitat](/guides/scopes-and-irreversibility). * **Tancament mensual segellat** — congela un mes finalitzat i segella'l amb una signatura RSA-SHA256 desacoblada sobre la instantània; el segellat és irreversible (un per tancament) i verificable de manera independent. Exporta el registre diari en el format `rdley_8_2019`, o un fitxer d'incidències per a nòmines A3, Sage o NominaSOL. Consulta [Tancament mensual del registre](/guides/monthly-time-close). * **Rol d'empleat només al portal** — un empleat fitxa, segueix un horari i sol·licita absències des del portal, i **mai** compta contra el límit `users` del pla. * **Add-on per assentament** — els empleats es facturen mitjançant una subscripció mensual dedicada (`employee-seats`) la quantitat de la qual segueix el teu cens actiu; contractar-la activa el mòdul. Un compte enterprise facturat per contracte l'obté gratis. Consulta [Facturació d'assentaments d'empleat](/guides/employee-seats). * **Paritat MCP** — cada ruta v1 reflecteix una tool MCP pública, així que un agent executa les mateixes operacions. Consulta el [catàleg de tools MCP](/mcp/tools#employee). Dos dominis són **de només lectura** via API — presència i festius exposen només lectures. Declarar presència a l'oficina o en remot i crear festius locals propis són tasques només del portal, sense scope `presence:write` ni `holidays:write`. ## API i MCP inclosos en tots els plans — 2026-07-04 [#api-i-mcp-inclosos-en-tots-els-plans--2026-07-04] L'API pública i el servidor MCP deixen de vendre's com a add-on `developer_api` a banda — ara estan **inclosos en tots els plans de Factuarea**: * **Tier per pla** — el teu tier de rate limit es deriva del teu pla: Emprendedor → `starter` (30 req/min, 5.000 req/mes), Empresario → `pro` (300 req/min, 50.000 req/mes), Enterprise → `scale` (personalitzat, sense topalls). Consulta [Límits de peticions](/guides/rate-limits). * **Trial inclòs** — durant el trial de 10 dies tens accés a l'API amb el tier `free` (10 req/min, 100 req/mes). * **Boost de capacitat** — si necessites més capacitat sense canviar de pla, subscriu-te des del dashboard a un tier estrictament superior al que atorga el teu pla; un tier igual o inferior retorna `422 boost_not_applicable`. Consulta [Boost de capacitat](/guides/rate-limits#capacity-boost). * **L'add-on desapareix** — els add-ons de developer Starter i Pro deixen de vendre's. El codi d'error `addon_not_active` es manté (ara significa que l'empresa no té un pla actiu que inclogui accés a l'API), així que les integracions existents no necessiten cap canvi. * **Programa beta tancat** — l'accés a l'API ja no se sol·licita: crea una key des de [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) i comença a cridar `/v1`. **v1** — publicada el 2026-05-03. Aquest és el primer llançament públic de la plataforma de Factuarea; tot el que segueix es publica junt. Els propers llançaments s'afegeixen a aquesta pàgina, del més recent al més antic, cada un encapçalat per la seva versió i data. Per primera vegada pots integrar Factuarea amb qualsevol sistema extern — per codi, per SDK, per línia de comandes o per agent d'IA — sense scraping ni macros. La superfície pública és un únic contracte a `https://api.factuarea.com/v1`, accessible de quatre maneres: l'API REST, els SDKs de TypeScript i PHP, el CLI `factuarea` i el servidor MCP. Cada superfície parla amb els mateixos recursos i aplica els mateixos scopes. ## REST API v1 [#rest-api-v1] L'API REST pública exposa ** operacions en recursos** com a JSON pla sobre HTTPS. Cada recurs s'identifica per una clau `id` opaca (un UUID v7). ### Documents de venda [#documents-de-venda] * **Factures** (`/v1/invoices`) — CRUD complet i el cicle de vida complet: enviar, marcar com a pagada, cancel·lar, anul·lar, duplicar, PDF i enllaç públic, cobraments i rebuts, recordatoris. Factures rectificatives amb els codis de motiu de rectificació `R1`–`R5`, elegibilitat i substitució de factura simplificada, emissió programada (schedule / reschedule / unschedule) i exportació trimestral (ZIP i email). Creació, enviament, canvi d'estat, esborrat i PDF en lot, a més d'exportació a Excel. * **Pressupostos** (`/v1/quotes`) — CRUD + acceptar, rebutjar, convertir a factura, PDF, enllaç públic. * **Factures proforma** (`/v1/proformas`) — CRUD + convertir a factura, PDF, enllaç públic. * **Albarans** (`/v1/delivery_notes`) — CRUD + signar, marcar com a lliurat, convertir a factura. * **Factures recurrents** (`/v1/recurring_invoices`) — CRUD + activar, pausar, reprendre, cancel·lar i previsualitzar la propera execució. ### Compres [#compres] * **Factures de compra** (`/v1/purchase_invoices`) — CRUD amb adjunt PDF, marcar com a pagada, registre de pagaments i informes de pendents / vençudes. ### CRM i catàleg [#crm-i-catàleg] * **Clients** (`/v1/clients`) — CRUD complet, cerca per NIF/CIF, verificació censal de l'AEAT i VIES, i importació CSV amb plantilla descarregable. * **Proveïdors** (`/v1/suppliers`) — CRUD complet, cerca per NIF/CIF. * **Productes** (`/v1/products`) — CRUD, cerca per SKU o external id, control d'stock (fixar, ajustar i actualització en lot), informe d'stock baix, analítica de vendes, i imatges de galeria i vídeo. * **Sèries de documents** (`/v1/series`) — sèries de numeració legal per tipus de document, amb reinici mensual / anual, selecció de predeterminada i arxivar / desarxivar. * **Impostos** (`/v1/taxes`) — tipus impositius (IVA, retenció d'IRPF, recàrrec d'equivalència) amb predeterminats per document. ### Compliment fiscal espanyol [#compliment-fiscal-espanyol] * **VeriFactu** (`/v1/verifactu/*`, `/v1/invoices/{invoice}/verifactu`) — registres de facturació, la cadena d'empremtes del SIF i la seva validació, subsanació (registres de correcció), la declaració responsable i el seu històric, i la gestió de certificats FNMT. * **FacturaE / FACe** (`/v1/invoices/{invoice}/facturae`, `/v1/face-submissions`) — descàrrega de l'XML FacturaE 3.2.2 i enviaments B2G a les administracions públiques mitjançant FACe (enviar, seguir, anul·lar). * **Cens de l'AEAT** (`/v1/account/census-verification`, `/v1/clients/*`) — verifica un NIF/CIF contra el registre de l'AEAT. * **Informes fiscals** (`/v1/tax_reports/*`) — genera, previsualitza, descarrega i mantén l'històric dels Models 303 (IVA), 347 (operacions anuals amb tercers) i 130 (pagament fraccionat d'IRPF). ### Pagaments [#pagaments] * **Autofacturació de Stripe** (`/v1/stripe-autoinvoicing/*`) — connecta comptes de Stripe i emet factures automàticament a partir dels pagaments de Stripe, incloses factures rectificatives automàtiques en les devolucions. * **Payouts i conciliació** (`/v1/payouts`, `/v1/connected-accounts`) — llegeix els payouts de Stripe i concilia les liquidacions, amb suport d'extractes bancaris Norma 43. ### Empreses gestionades (gestories) [#empreses-gestionades-gestories] * **Empreses** (`/v1/companies`) — aprovisiona i opera empreses filles des d'un compte mestre: crear, activar, desactivar, seguir l'estat de creació i emetre API keys per empresa (crear, rotar, revocar). Previsualitza el cost per seat abans de confirmar amb `/v1/companies/seat-charge-preview`. Opera en nom d'una filla en una sola petició amb el header `X-Active-Profile`. ### Webhooks i esdeveniments [#webhooks-i-esdeveniments] * **Webhooks** (`/v1/webhook_endpoints` amb `deliveries` imbricats) — endpoints subscribibles signats amb HMAC SHA256, rotació de doble secret, ping / test, i un històric d'entregues que pots reenviar. * **Esdeveniments** (`/v1/events`, `/v1/event-catalog`) — el flux històric d'esdeveniments i el catàleg de tipus d'esdeveniment subscribibles. ### Compte [#compte] * **Compte** (`/v1/account`) — introspecciona la credencial autenticada (empresa, pla, scopes i tier de límit de peticions), gestiona API keys, personalitza les plantilles de document i executa la teva pròpia verificació censal. ## Fonaments de l'API [#fonaments-de-lapi] Comportament que comparteixen tots els recursos, així una integració l'aprèn una sola vegada: * **Mode de prova** — les claus `fact_test_*` s'executen contra una empresa sandbox aïllada; els efectes externs (VeriFactu/AEAT, FACe, email, webhooks) no s'executen, així crees i proves sense tocar les dades de producció. * **Identificadors opacs** — cada recurs exposa una clau `id` el valor de la qual és un UUID v7, amb foreign keys com a `*_id`. * **Paginació per cursor** — `starting_after` / `ending_before`, sense `?page=`. * **Idempotència** — el header `Idempotency-Key` (màx. 64 caràcters, TTL de 24 h); una petició repetida retorna la resposta original emmagatzemada — inclosa una `4xx` en memòria cau — marcada amb `Idempotent-Replayed`. * **Límits de peticions** — quotes per tier, per minut i mensuals, amb headers `X-RateLimit-*`. * **Errors normalitzats** — l'embolcall `{ error: { type, code, message, param, request_id, doc_url } }`; els errors de validació assenyalen el camp problemàtic mitjançant `param`. Ramifica segons `code`, mai segons el `message` orientat a persones. * **Operacions en lot** — els endpoints per lots informen de l'èxit parcial per element, així una fila incorrecta no fa fallar tota la petició. * **Importació i exportació** — importació CSV de clients (amb plantilla descarregable) i exportació de factures a Excel. * **Webhooks signats** — HMAC SHA256 amb ±5 min de tolerància i reintents exponencials fins a 8 intents. * **Scopes** — un catàleg tancat `resource:action`; tota operació a la qual no pots accedir queda oculta, i els scopes destructius `write` / `delete` es marquen com a sensibles a la pantalla de consentiment d'OAuth i mai es pre-marquen. * **Versionat** — el prefix d'URL `/v1` més un header `Factuarea-Version` fixat. `/v1` es manté estable durant almenys 24 mesos; qualsevol breaking change viu a `/v2` amb una finestra de coexistència d'almenys 12 mesos. ## SDKs oficials — TypeScript i PHP [#sdks-oficials--typescript-i-php] Els SDKs mantinguts envolten tota l'API REST v1 amb un runtime premium, així no escrius HTTP a mà. Consulta la [secció de SDKs](/sdks). * **TypeScript / Node.js** — [`@factuarea/sdk`](https://www.npmjs.com/package/@factuarea/sdk) a npm. ESM + CommonJS dual, declaracions de tipus completes, Node 20+ (i Deno / Bun / Workers). Codi font: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). * **PHP** — [`factuarea/factuarea-php`](https://packagist.org/packages/factuarea/factuarea-php) a Packagist. PSR-4, basat en Guzzle, PHP 8.2+. Codi font: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). ```bash npm install @factuarea/sdk composer require factuarea/factuarea-php ``` Tots dos comparteixen el mateix runtime: reintents automàtics (amb backoff, respectant `Retry-After`), claus d'idempotència automàtiques, auto-paginació per cursor, una jerarquia d'[errors](/guides/errors) tipada, verificació de webhooks en temps constant i descàrregues binàries (PDF). Cada pàgina de la referència de l'API mostra un snippet de TypeScript, PHP i cURL llest per copiar. Cada release fixa una [`Factuarea-Version`](/guides/versioning) i l'envia en cada request. ## Interfície de línia de comandes [#interfície-de-línia-de-comandes] El [CLI `factuarea` oficial](/cli) (`v0.1.3`) opera tota la superfície v1 des del teu terminal. És **agent-first** — sortida JSON estable, exit codes semàntics i descobriment en una sola crida — i l'arbre de comandes es genera des de l'spec OpenAPI, així que mai es desincronitza de la superfície en viu. * **Una clau, dos entorns** — el prefix de la clau selecciona l'entorn; una mutació `fact_live_` requereix a més el flag explícit `--live` com a xarxa de seguretat. * **Devloop** — `listen` reenvia esdeveniments a la teva màquina i `trigger` produeix esdeveniments reals de sandbox, així proves webhooks en local sense túnel ni ngrok. * **Instal·lació** — Homebrew, npm o un instal·lador `curl`. Consulta el [CLI](/cli). ## Servidor MCP per a agents d'IA [#servidor-mcp-per-a-agents-dia] El [servidor MCP](/mcp) a `https://mcp.factuarea.com` exposa l'API pública com a ** tools de Model Context Protocol** sobre el transport **Streamable HTTP**, així els agents d'IA (Claude i altres) les descobreixen i les criden sense que hagis de cablejar cada endpoint. * **Dos canals d'auth** — una **API key** (`fact_live_` / `fact_test_`) per al propietari del compte (fins a les tools), o **OAuth 2.1** per a apps de tercers (un catàleg curat de tools). Consulta [Connectar un client](/mcp/connect#authenticate). * **OAuth 2.1 complet** — Dynamic Client Registration (RFC 7591), PKCE (S256), una pantalla de consentiment amb selecció d'empresa **i** entorn, rotació de refresh-token amb detecció de reutilització, a més de revocació i introspecció. * **Governat per scopes** — cada tool aplica un scope granular; les tools a les quals no pots accedir queden ocultes a `tools/list`. Consulta [Scopes i permisos](/mcp/scopes). * **Errors fidels a v1** — els errors JSON-RPC conserven el mateix `code` i `http_status` que l'API REST. Consulta [Errors i límits de peticions](/mcp/errors). * **Claude Code** — el plugin oficial `factuarea-mcp` [plugin](/mcp/claude-code-plugin) connecta en dues comandes. * **Mode de prova** — executa-ho tot contra el sandbox aïllat. Consulta [Mode de prova](/mcp/connect#test-mode). ## Comença en mode de prova [#comença-en-mode-de-prova] La regla d'or a les quatre superfícies: **comença en mode de prova**. Crea contra una clau `fact_test_` (o un consentiment OAuth amb l'entorn Test), després canvia a `fact_live_` — sense canvis de codi. Benvingut a l'era de les integracions a Factuarea. --- # Resum de la CLI (/ca/cli) El CLI oficial **`factuarea`** maneja l'[API REST v1](/api-reference/account/public-api.v1.account.show) des del teu terminal. És **agent-first** — sortida JSON estable, exit codes semàntics i descobriment en una sola crida — i inspirat en Stripe: l'arbre de comandes complet es genera des de l'especificació OpenAPI, així que mai es desincronitza de la superfície real. L'última versió estable és **`v0.1.3`**. Prefereixes que un agent d'IA manegi Factuarea directament? El CLI està fet per a això. Consulta [Agents i scripting](/cli/agents) per al contracte JSON i els exit codes, i el [servidor MCP](/mcp) per a l'alternativa basada en tools. ## Instal·lació [#installació] macOS i Linux: ```bash brew install --cask factuarea/tap/factuarea ``` Qualsevol plataforma amb **Node 20 o superior**: ```bash npm i -g @factuarea/cli # o: npx @factuarea/cli ``` Instal·la un binari signat a `~/.local/bin`: ```bash curl -fsSL https://github.com/factuarea/factuarea-cli/releases/latest/download/install.sh | sh ``` Els binaris estan signats (cosign) i venen amb `checksums.txt` a [Releases](https://github.com/factuarea/factuarea-cli/releases). Requereix **Go 1.26 o superior**: ```bash git clone https://github.com/factuarea/factuarea-cli && cd factuarea-cli make build # genera ./factuarea ``` La notarització a macOS i la signatura Authenticode a Windows arriben en una fase posterior. De moment, a macOS fes servir `brew` o `npm`, o executa `xattr -d com.apple.quarantine ./factuarea` sobre un binari solt. ## Autenticació [#autenticació] El CLI fa servir la teva **API key** de Factuarea. El prefix de la key decideix l'entorn — no hi ha cap flag a part: * `fact_test_…` → el [sandbox](/guides/test-mode) aïllat: dades de prova, sense efectes reals (no transmet a l'AEAT, no envia email, no entrega webhooks). * `fact_live_…` → producció: dades reals. **Inicia sessió** ```bash factuarea login # et demana la key en un prompt ocult ``` La key es llegeix en un prompt ocult — mai es passa com a argument visible. Es desa al keyring del sistema (amb fallback a `~/.config/factuarea/config.toml`, permisos 600). Suporta múltiples **perfils** amb `--profile`. **O defineix una variable d'entorn** Per a entorns no interactius: ```bash export FACTUAREA_API_KEY=fact_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` **Verifica** ```bash factuarea whoami # mostra el compte i l'entorn (TEST/LIVE) ``` Comença tota integració amb una key **`fact_test_`**. La superfície de comandes és idèntica a producció — canvia el prefix a `fact_live_` només quan el teu flux funcioni de principi a fi. Les mutacions en producció (amb una key `fact_live_`) requereixen a més el flag explícit `--live` com a xarxa de seguretat. ## Què segueix [#què-segueix] L'arbre de comandes generat — list, show, create, accions de domini, descàrregues binàries, l'escape hatch `api` i `commands --json`. Prova webhooks en local sense desplegar ni ngrok: `listen` reenvia els esdeveniments a la teva màquina, `trigger` produeix esdeveniments reals en sandbox. El contracte agent-first: JSON estable per stdout, errors estructurats per stderr, exit codes semàntics, scope-check i confirmació tipada. --- # Agents i scripting (/ca/cli/agents) El CLI és **agent-first**: un assistent d'IA o un script pot descobrir tota la superfície en una crida, obtenir sortida estable llegible per màquina, i ramificar segons exit codes semàntics en lloc de parsejar prosa. ## Descobreix la superfície en una crida [#descobreix-la-superfície-en-una-crida] ```bash factuarea commands --json ``` Això aboca el manifest complet de comandes. Cada entrada porta: | Camp | Significat | | ---------------- | ----------------------------------------------------------- | | `path` | El path de la comanda, p. ex. `invoices create`. | | `args` | Arguments posicionals (path params). | | `flags` | Flags disponibles. | | `mutating` | Si la comanda escriu (necessita `--live` en producció). | | `binary` | Si retorna un binari (PDF/ZIP/XML) en lloc de JSON. | | `paginated` | Si la comanda suporta paginació per cursor. | | `required_scope` | El scope que l'API key ha de tenir, p. ex. `invoices:read`. | | `irreversible` | Si l'operació no es pot desfer. | | `example` | Una invocació d'exemple llesta per adaptar. | `required_scope` i `irreversible` vénen directament de les extensions `x-required-scope` i `x-irreversible` de l'especificació OpenAPI, així que el CLI i la [referència de l'API](/api-reference/account/public-api.v1.account.show) coincideixen per construcció. ## Contracte de sortida [#contracte-de-sortida] * `--json` emet el **cos cru de l'API** per **stdout**. * Els errors van a **stderr** com a JSON estructurat — el mateix [embolcall d'error](/guides/errors) que l'API: `error.{type,code,message,request_id,doc_url}`. * Reserva stdout per a les dades i stderr per als diagnòstics: canalitza stdout a `jq`, registra stderr. ## Exit codes [#exit-codes] Ramifica segons l'exit code, mai segons el missatge: | Codi | Significat | | ---- | ------------------------ | | `0` | OK | | `2` | Error d'ús / guard local | | `3` | Error d'autenticació | | `4` | Permís / scope absent | | `5` | Error de validació | | `6` | No trobat | | `7` | Límit de peticions | | `8` | Conflicte / idempotència | | `9` | Error del servidor | | `10` | Xarxa / timeout | ## Scope-check local [#scope-check] Abans d'una crida, el CLI comprova que la teva key té el `required_scope` de l'operació. Si no el té, la comanda **falla en local amb exit `4`** i un missatge clar — sense gastar un round trip que l'API rebutjaria amb `403` de tota manera. * La comprovació només corre quan l'operació declara un scope i resol els scopes de la key de manera lazy (com a molt un `GET /v1/account` per invocació, memoïtzat). * Un scope `*` a la key cobreix qualsevol operació. * `--skip-scope-check` degrada el bloqueig a un avís i continua — útil si els teus scopes en cache estan desactualitzats. El `403` real de l'API segueix sent l'última línia de defensa. ## Operacions irreversibles [#irreversible-operations] Les operacions que l'especificació marca `x-irreversible` (esborrats, `void`, conversions terminals, emissió fiscal, rotació de certificat, oblit GDPR…) demanen una confirmació tipada abans de la crida: ```bash factuarea invoices delete --confirm ``` * Passa `--confirm ` amb l'id del recurs per continuar. * En un context no interactiu (`--no-input` o sense TTY) sense `--confirm`, la comanda es nega amb exit `2` en lloc d'endevinar. Consulta la [guia de scopes i irreversibilitat](/guides/scopes-and-irreversibility) per a la llista completa de quines operacions porten cada scope i quines són irreversibles. Vols que l'agent manegi Factuarea a través de tools en lloc de comandes del CLI? Connecta'l al [servidor MCP](/mcp) — la mateixa superfície exposada com a tools, amb autenticació OAuth i per API key. --- # Devloop (/ca/cli/devloop) Prova els teus webhooks en local sense desplegar ni ngrok, a l'estil del CLI de Stripe. El bucle té dues meitats: **`listen`** reenvia els esdeveniments del teu compte a la teva màquina, **`trigger`** produeix esdeveniments reals en sandbox per reenviar. ## Reenviar esdeveniments a localhost [#reenviar-esdeveniments-a-localhost] ```bash factuarea listen --forward-to http://localhost:3000/webhooks ``` `listen` sondeja el feed d'esdeveniments, reconstrueix el cos del webhook i el signa amb HMAC (`Factuarea-Signature`) fent servir un secret efímer `whsec_…` que imprimeix en arrencar. Configura aquest secret al teu verificador i el teu codi de verificació corre sense canvis — sense diferències de codi entre local i producció. Per seguretat, `listen` només reenvia a `localhost`. Per reenviar a un host remot, passa `--allow-remote-forward` explícitament. ## Produir esdeveniments per provar [#produir-esdeveniments-per-provar] En una altra terminal, produeix esdeveniments reals al sandbox: ```bash factuarea trigger invoice.paid factuarea trigger --list # esdeveniments suportats ``` `trigger` només opera al **sandbox** — requereix una key `fact_test_`. Mai produeix esdeveniments contra dades de producció. ## Per què la verificació queda idèntica [#per-què-la-verificació-queda-idèntica] L'esquema de signatura és el mateix que fa servir la plataforma, així que el verificador que desplegues a producció és el verificador amb el qual proves en local: * HMAC-SHA256 sobre el cos cru amb una comparació de temps constant. * Una tolerància de timestamp que rebutja els reenviaments. * Ambdues signatures acceptades durant una finestra de gràcia de rotació del secret. L'única diferència és el secret: en local és el `whsec_…` efímer de `listen`; en producció és el secret de l'endpoint. Consulta [Webhooks](/guides/webhooks) per al contracte de signatura complet i els verificadors dels SDKs. Una fase futura substitueix el sondeig de `listen` per un relay WebSocket. La superfície de comandes queda igual; només canvia el transport. --- # Ús (/ca/cli/usage) L'arbre de comandes cobreix tots els recursos de l'API (`factuarea [] `), generat des de l'especificació OpenAPI perquè mai es desincronitzi de la superfície real. ## Llegir dades [#llegir-dades] ```bash # Llistar (amb paginació automàtica per cursor) factuarea invoices list --json factuarea clients list --paginate --json # Obtenir-ne un factuarea invoices show --json ``` `--json` emet el cos cru de l'API per **stdout**. `--paginate` recorre totes les pàgines per tu, seguint `next_cursor` fins que `has_more` sigui fals. Consulta [Paginació](/guides/pagination) per a la semàntica del cursor subjacent. ## Escriure dades [#escriure-dades] Passa el cos JSON amb `-d` (en línia) o `--data-file` (una ruta). L'API calcula els totals — no els arrodoneixis per endavant. ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","lines":[…]}' ``` Cada mutació rep un `Idempotency-Key` automàtic perquè una petició reintentada mai creï el recurs dues vegades. Consulta [Idempotència](/guides/idempotency). ## Accions de domini [#accions-de-domini] Els canvis d'estat són **accions discretes**, no un flag d'estat genèric — reflectint el disseny propi de l'API: ```bash factuarea invoices send factuarea invoices mark-paid ``` Algunes accions són **irreversibles** (esborrats, `void`, conversions, emissió fiscal). El CLI et demana confirmar-les abans de la crida — consulta [Operacions irreversibles](/cli/agents#irreversible-operations) i la [guia de scopes i irreversibilitat](/guides/scopes-and-irreversibility). ## Control horari (fitxatges i absències) [#control-horari-fitxatges-i-absències] L'add-on de control horari afegeix els recursos de jornada — empleats, horaris, fitxatges, absències, presència, festius, tancaments mensuals i el resum de gestoria. Cada comanda es genera des de l'especificació i queda protegida pel seu scope fi (`employees:*`, `time_entries:*`, `absences:*`, `work_schedules:*`, `presence:read`, `holidays:read`, `payroll_exports:*`). ```bash # Fitxar entrada i sortida (cada assentament encadena la seva empremta — RD-llei 8/2019) factuarea time-entries clock-in -d '{"employee_id":"…","source":"web"}' factuarea time-entries clock-out -d '{"employee_id":"…","source":"web"}' # Sol·licitar una absència i aprovar-la factuarea absence-requests create \ -d '{"employee_id":"…","absence_type_id":"…","start_date":"2026-08-01","end_date":"2026-08-05"}' factuarea absence-requests approve # Presència de l'equip en viu factuarea presence live --json # Tancar el registre mensual inalterable i exportar-lo (ITSS RD-llei 8/2019) factuarea monthly-time-record-closes create -d '{"year":2026,"month":7}' factuarea monthly-time-record-closes export --format rdley_8_2019 --json ``` ## Descàrregues binàries i pujades [#descàrregues-binàries-i-pujades] Els endpoints de PDF, ZIP i XML transmeten un binari que deses amb `-o`. Les pujades multipart prenen el fitxer amb un flag `--file-`: ```bash # Descarregar un PDF factuarea invoices pdf -o invoice.pdf # Pujar un certificat (multipart) factuarea verifactu certificates upload \ -d '{"certificate_password":"…"}' --file-certificate_file cert.p12 ``` ## L'escape hatch `api` [#lescape-hatch-api] Qualsevol endpoint és accessible directament amb `factuarea api `, fins i tot els que encara no tenen una comanda dedicada: ```bash factuarea api get /v1/account --json factuarea api post /v1/invoices -d '{…}' ``` ## El manifest de comandes [#el-manifest-de-comandes] `factuarea commands --json` aboca el **manifest complet** de comandes en una sola crida — path, args, flags, si cadascuna muta, si és binària o paginada, el seu scope requerit, si és irreversible, i un exemple. Un agent descobreix tota la superfície en una sola crida: ```bash factuarea commands --json ``` Consulta [Agents i scripting](/cli/agents) per als camps del manifest i el contracte JSON. ## Referència de l'API incrustada [#referència-de-lapi-incrustada] Una referència ràpida de l'API viatja amb el binari — les cerques no surten de la teva màquina: ```bash factuarea docs search invoice ``` `docs search` consulta l'**especificació OpenAPI incrustada al binari** i respon a «quina comanda crido?». Retorna *operacions* — comanda, resum, mètode i ruta — i no toca mai la xarxa. ## Cercar a la documentació publicada [#cercar-a-la-documentació-publicada] `docs list`, `docs grep` i `docs get` consulten la **documentació publicada** —el corpus `llms-full` de [docs.factuarea.com](https://docs.factuarea.com)— i responen a «què diu la documentació sobre això?». Retornen *pàgines i seccions*, de les guies, la referència de l'API i el catàleg d'errors: ```bash factuarea docs list # totes les pàgines: factuarea docs list /guides # només les que pengen d'aquest prefix factuarea docs grep "idempotency-key" # seccions de documentació que coincideixen factuarea docs get /guides/idempotency # la pàgina sencera, en Markdown ``` El corpus es descarrega **sencer i una sola vegada**, es desa al directori de memòria cau del sistema (`~/Library/Caches/factuarea/docs/` a macOS, `~/.cache/factuarea/docs/` a Linux) i es filtra en local. Mentre la còpia tingui menys de **15 minuts**, no hi ha cap petició de xarxa, així que una sessió que encadeni `list`, `grep` i `get` descarrega una vegada. **El teu terme de cerca no surt mai de la màquina.** La URL que es demana és fixa i no depèn del que teclegis — no hi ha cap servidor de cerca a l'altre costat. Cap de les quatre subcomandes de `docs` llegeix ni envia una API key. | Opció | Què fa | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `--refresh` | Torna a descarregar, ignorant una còpia encara vigent | | `--lang` | Idioma de les guies: `en`, `es` o `ca` (per defecte `en`, l'idioma font). La referència de l'API no es tradueix i surt sempre | | `--json` | Sortida estable per stdout: `path`/`title` a `list`, `path`/`title`/`section`/`snippet` a `grep`, `path`/`title`/`markdown` a `get` | Si la descàrrega falla i hi ha una còpia a la memòria cau —encara que hagi caducat—, es fa servir aquesta còpia, l'avís va a **stderr** perquè el JSON de stdout continuï essent parsejable, i l'exit code és `0`. Sense cap còpia, l'exit code és `10` (xarxa). Apunta `FACTUAREA_DOCS_URL` a un altre origen per descarregar el corpus des d'allà. --- # Codis d'error per categoria (/ca/errors) Cada `code` d'error és estable entre versions i té la seva pròpia pàgina amb la causa i l'acció a prendre. Tria una categoria, o obre la taula de referència completa. | Categoria | Codis | | ---------------------------------------------------------- | ----- | | [Albarans](/ca/errors/index-delivery-notes) | 4 | | [Autenticació](/ca/errors/index-authentication) | 7 | | [Autorització](/ca/errors/index-authorization) | 9 | | [Clients](/ca/errors/index-clients) | 9 | | [Compte](/ca/errors/index-account) | 3 | | [Empleats](/ca/errors/index-employees) | 2 | | [Empreses](/ca/errors/index-companies) | 5 | | [Events](/ca/errors/index-events) | 1 | | [Factures](/ca/errors/index-invoices) | 40 | | [Factures de compra](/ca/errors/index-purchase-invoices) | 15 | | [Factures proforma](/ca/errors/index-proformas) | 18 | | [Factures recurrents](/ca/errors/index-recurring-invoices) | 15 | | [Idempotency](/ca/errors/index-idempotency) | 3 | | [Impostos](/ca/errors/index-taxes) | 26 | | [Informes fiscals](/ca/errors/index-tax-reports) | 6 | | [Límit de peticions](/ca/errors/index-rate-limit) | 2 | | [Notificacions](/ca/errors/index-notifications) | 1 | | [Pagaments](/ca/errors/index-payments) | 5 | | [Pressupostos](/ca/errors/index-quotes) | 4 | | [Productes](/ca/errors/index-products) | 6 | | [Proveïdors](/ca/errors/index-suppliers) | 2 | | [Request](/ca/errors/index-request) | 38 | | [Series](/ca/errors/index-series) | 17 | | [Servidor](/ca/errors/index-server) | 9 | | [VeriFactu](/ca/errors/index-verifactu) | 25 | | [Webhooks](/ca/errors/index-webhooks) | 13 | ## Relacionat [#relacionat] * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # account_not_found (/ca/errors/account_not_found) | Code | Type | HTTP | Categoria | | ------------------- | ----------------- | ---- | ---------------------------------- | | `account_not_found` | `not_found_error` | 404 | [Compte](/ca/errors/index-account) | ## Causa [#causa] No es va poder resoldre el compte associat a la clau, cosa que sol voler dir que la clau ja no apunta a una empresa viva. ## Què fer [#què-fer] Comprova que la clau pertany a una empresa activa i torna a emetre-la si l'empresa va canviar. ## Relacionat [#relacionat] * [Tots els codis d'error de Compte](/ca/errors/index-account) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # addon_not_active (/ca/errors/addon_not_active) | Code | Type | HTTP | Categoria | | ------------------ | --------------------- | ---- | ---------------------------------------------- | | `addon_not_active` | `authorization_error` | 403 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] La funcionalitat pertany a un add-on que ara mateix no està actiu per a l'empresa. ## Què fer [#què-fer] Contracta o renova l'add-on; a diferència d'un problema d'abast, cap clau dona accés a una funcionalitat no contractada. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El teu pla actual no inclou accés a l'API pública. Contracta o renova un pla de Factuarea per fer servir l'API. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # addon_required (/ca/errors/addon_required) | Code | Type | HTTP | Categoria | | ---------------- | ------------------------ | ---- | ------------------------------------- | | `addon_required` | `payment_required_error` | 402 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] Crear endpoints de webhook pertany a l'add-on Developer API, i l'empresa no el té actiu: el nivell gratuït permet zero endpoints. ## Què fer [#què-fer] Contracta l'add-on i repeteix la crida; a diferència d'un problema de permisos, aquí el que falta és la contractació, no l'abast. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # alta_record_not_found (/ca/errors/alta_record_not_found) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------- | ---- | --------------------------------------- | | `alta_record_not_found` | `not_found_error` | 404 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] La factura no té registre d'alta, així que l'operació que en depèn no té sobre què treballar. ## Què fer [#què-fer] Comprova que la factura es va emetre amb VeriFactu actiu; si el registre va quedar diferit per un problema de certificat, arregla el certificat i es crearà. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # alternative_id_type_invalid (/ca/errors/alternative_id_type_invalid) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ----------------------------------- | | `alternative_id_type_invalid` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] El tipus d'identificador alternatiu queda fora del catàleg `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. ## Què fer [#què-fer] Envia el tipus que correspon al document que estàs registrant; es declara a l'AEAT juntament amb l'identificador. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # anulacion_record_already_exists (/ca/errors/anulacion_record_already_exists) | Code | Type | HTTP | Categoria | | --------------------------------- | ---------------- | ---- | --------------------------------------- | | `anulacion_record_already_exists` | `conflict_error` | 409 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] La factura ja té un registre d'anul·lació a la cadena, i l'anul·lació es declara una sola vegada. ## Què fer [#què-fer] Llegeix el registre existent per comprovar-ne l'estat AEAT en lloc de tornar a anul·lar. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # api_key_already_revoked (/ca/errors/api_key_already_revoked) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `api_key_already_revoked` | `invalid_request_error` | 422 | [Compte](/ca/errors/index-account) | ## Causa [#causa] La clau ja estava revocada, i una clau revocada no admet més operacions: la revocació és terminal. ## Què fer [#què-fer] Emet una clau nova si necessites credencials un altre cop; en aquesta no queda res a revocar ni a rotar. ## Relacionat [#relacionat] * [Tots els codis d'error de Compte](/ca/errors/index-account) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # api_key_expired (/ca/errors/api_key_expired) | Code | Type | HTTP | Categoria | | ----------------- | ---------------------- | ---- | ----------------------------------------------- | | `api_key_expired` | `authentication_error` | 401 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] La clau va passar la seva data de caducitat. ## Què fer [#què-fer] Emet una clau nova; si fas servir dates de caducitat, planifica la rotació abans de la data perquè la integració no es quedi a les fosques. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Aquesta clau API ha caducat. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # api_key_not_found (/ca/errors/api_key_not_found) | Code | Type | HTTP | Categoria | | ------------------- | ----------------- | ---- | ---------------------------------- | | `api_key_not_found` | `not_found_error` | 404 | [Compte](/ca/errors/index-account) | ## Causa [#causa] L'identificador no correspon a cap clau API de l'empresa autenticada. ## Què fer [#què-fer] Llista les teves claus i fes servir l'`id` que retornen; el secret d'una clau mai és un identificador vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de Compte](/ca/errors/index-account) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # api_key_revoked (/ca/errors/api_key_revoked) | Code | Type | HTTP | Categoria | | ----------------- | ---------------------- | ---- | ----------------------------------------------- | | `api_key_revoked` | `authentication_error` | 401 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] La clau va ser revocada, i una clau revocada no torna a autenticar mai: revocar és justament la manera de tallar una credencial filtrada. ## Què fer [#què-fer] Emet una clau nova i desplega-la allà on hi hagués l'antiga. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Aquesta clau API ha estat revocada. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # api_version_invalid_format (/ca/errors/api_version_invalid_format) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------------- | ---- | ------------------------------------- | | `api_version_invalid_format` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] La versió de payload de l'endpoint no és una data `YYYY-MM-DD`. ## Què fer [#què-fer] Envia la versió com a data, coincidint amb una de les versions de payload publicades. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # api_version_unsupported (/ca/errors/api_version_unsupported) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ------------------------------------- | | `api_version_unsupported` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] La versió de payload està ben formada però no és entre les que serveix la plataforma. ## Què fer [#què-fer] Tria una versió suportada, o deixa el camp fora per rebre els esdeveniments en la vigent. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # attachment_invalid_filename (/ca/errors/attachment_invalid_filename) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `attachment_invalid_filename` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] El nom del fitxer no és utilitzable: és buit, porta components de ruta, o supera els 200 caràcters. ## Què fer [#què-fer] Envia un nom de fitxer simple amb la seva extensió, sense directoris ni segments `../`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # attachment_mime_not_allowed (/ca/errors/attachment_mime_not_allowed) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `attachment_mime_not_allowed` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] El tipus de fitxer queda fora del conjunt admès: PDF, PNG, JPEG, XML i HTML. ## Què fer [#què-fer] Converteix el document a PDF o envia l'original que va emetre el proveïdor; els fulls de càlcul i els documents d'ofimàtica no s'accepten com a adjunt fiscal. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # attachment_missing (/ca/errors/attachment_missing) | Code | Type | HTTP | Categoria | | -------------------- | ----------------- | ---- | -------------------------------------------------------- | | `attachment_missing` | `not_found_error` | 404 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] La factura de compra existeix però no té fitxer adjunt, així que no hi ha res a descarregar. ## Què fer [#què-fer] Puja el document del proveïdor a la factura abans de demanar el fitxer. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # attachment_too_large (/ca/errors/attachment_too_large) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `attachment_too_large` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] El fitxer supera la mida màxima permesa per a un adjunt de document. ## Què fer [#què-fer] Comprimeix el PDF o abaixa la resolució de l'escaneig abans de pujar-lo. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # business_rule_violation (/ca/errors/business_rule_violation) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `business_rule_violation` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Una invariant del domini va rebutjar l'operació. Aquest codi indica la família; `error.subcode` anomena la regla concreta i `error.message` l'explica. ## Què fer [#què-fer] Busca el `error.subcode` a la referència d'errors: el payload pot ser correcte i l'operació seguir sense estar permesa en l'estat actual. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # cannot_archive_last_default_series (/ca/errors/cannot_archive_last_default_series) | Code | Type | HTTP | Categoria | | ------------------------------------ | ----------------------- | ---- | --------------------------------- | | `cannot_archive_last_default_series` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] La sèrie és l'única activa del seu tipus de document. Arxivar-la deixaria l'empresa sense numeració disponible i congelaria aquest tipus de document. ## Què fer [#què-fer] Crea una altra sèrie del mateix tipus, marca-la com a default i arxiva aquesta després. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # cannot_attach_to_cancelled_purchase_invoice (/ca/errors/cannot_attach_to_cancelled_purchase_invoice) | Code | Type | HTTP | Categoria | | --------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `cannot_attach_to_cancelled_purchase_invoice` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] La factura està cancel·lada, i adjuntar documents a un registre cancel·lat alteraria documentació ja tancada. ## Què fer [#què-fer] Torna a registrar la despesa en una factura viva i adjunta-hi el fitxer. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # cannot_have_both_tax_id_and_alternative_id (/ca/errors/cannot_have_both_tax_id_and_alternative_id) | Code | Type | HTTP | Categoria | | -------------------------------------------- | ----------------------- | ---- | ----------------------------------- | | `cannot_have_both_tax_id_and_alternative_id` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] El client envia `tax_id` i un identificador alternatiu alhora. La identitat fiscal és una: l'identificador alternatiu existeix precisament per a parts sense NIF espanyol. ## Què fer [#què-fer] Deixa `tax_id` per a parts espanyoles, o l'identificador alternatiu amb el seu tipus per a les estrangeres, i buida l'altre camp. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # census_requires_tax_id (/ca/errors/census_requires_tax_id) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `census_requires_tax_id` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] La verificació censal contrasta el parell nom + NIF contra l'AEAT, i en falta un dels dos. ## Què fer [#què-fer] Omple el NIF de la part que es verifica abans de demanar la comprovació. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # certificate_expired (/ca/errors/certificate_expired) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------------- | | `certificate_expired` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El certificat està fora de la seva finestra de validesa: ha caducat, o encara no és vàlid. ## Què fer [#què-fer] Renova el certificat a la FNMT i puja el nou; en pujar-ne un de vàlid es reencuen els registres que van quedar pendents. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # certificate_nif_mismatch (/ca/errors/certificate_nif_mismatch) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | --------------------------------------- | | `certificate_nif_mismatch` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El NIF del titular del certificat no coincideix amb el de l'empresa. Els registres AEAT es signen en nom de l'empresa, així que tots dos han de ser el mateix. ## Què fer [#què-fer] Puja el certificat emès per al NIF d'aquesta empresa, o corregeix el `tax_id` de l'empresa si és aquí on hi ha l'error. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # certificate_not_found (/ca/errors/certificate_not_found) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------- | ---- | --------------------------------------- | | `certificate_not_found` | `not_found_error` | 404 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'empresa no té cap certificat FNMT que correspongui a l'identificador, o no en té cap de pujat. ## Què fer [#què-fer] Puja el certificat `.p12` de l'empresa; sense ell no es pot signar ni transmetre cap registre. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # certificate_too_large (/ca/errors/certificate_too_large) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | --------------------------------------- | | `certificate_too_large` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El fitxer supera el límit de 100 KB, quan un certificat FNMT real pesa uns pocs kilobytes. ## Què fer [#què-fer] Assegura't de pujar el certificat en si i no un paquet, un arxiu comprimit o una còpia de seguretat que el contingui. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # client_has_documents (/ca/errors/client_has_documents) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ----------------------------------- | | `client_has_documents` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] El client està referenciat per documents emesos. Esborrar-lo deixaria factures, pressupostos o albarans sense la part a qui es van emetre, i els registres fiscals han de seguir sent traçables. ## Què fer [#què-fer] Desactiva el client en lloc d'esborrar-lo: deixa d'aparèixer als selectors i els seus documents conserven la referència. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # client_import_too_large (/ca/errors/client_import_too_large) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `client_import_too_large` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] El CSV supera el límit de files que admet la importació síncrona, ja que el fitxer sencer es processa dins de la mateixa petició. ## Què fer [#què-fer] Parteix el fitxer en lots més petits i importa'ls l'un després de l'altre. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # client_not_found (/ca/errors/client_not_found) | Code | Type | HTTP | Categoria | | ------------------ | ----------------- | ---- | ----------------------------------- | | `client_not_found` | `not_found_error` | 404 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] L'identificador no resol a cap client de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu, o busca el client per `tax_id` o per `external_id` abans de crear un duplicat. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # client_requires_tax_identity (/ca/errors/client_requires_tax_identity) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ----------------------------------- | | `client_requires_tax_identity` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] El client no té identitat fiscal: ni `tax_id` ni identificador alternatiu, i no es pot emetre una factura a una part sense identificar. ## Què fer [#què-fer] Omple `tax_id`, o un identificador alternatiu amb el seu tipus quan el client no tingui NIF espanyol. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # clock_drift_exceeded (/ca/errors/clock_drift_exceeded) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | --------------------------------------- | | `clock_drift_exceeded` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El rellotge del servidor es va desviar de l'NTP per sobre del marge permès. La marca de temps de generació entra a l'empremta AEAT, així que un rellotge desincronitzat produiria registres que l'AEAT rebutja. ## Què fer [#què-fer] És una condició del costat del servidor, no un problema del payload: reintenta d'aquí a uns minuts i, si persisteix, comunica el `request_id` a suport. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # company_inactive (/ca/errors/company_inactive) | Code | Type | HTTP | Categoria | | ------------------ | --------------------- | ---- | -------------------------------------- | | `company_inactive` | `authorization_error` | 403 | [Empreses](/ca/errors/index-companies) | ## Causa [#causa] El perfil que indica `X-Active-Profile` és una de les teves empreses gestionades, però està desactivada i no es pot operar fins que torni a estar activa. ## Què fer [#què-fer] Reactiva l'empresa gestionada, o apunta la capçalera a un altre perfil. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empreses](/ca/errors/index-companies) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # conflicting_pagination_params (/ca/errors/conflicting_pagination_params) | Code | Type | HTTP | Categoria | | ------------------------------- | ----------------------- | ---- | ----------------------------------- | | `conflicting_pagination_params` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] `starting_after` i `ending_before` van viatjar a la mateixa petició. Recorren la col·lecció en sentits oposats, així que només se'n pot aplicar un. ## Què fer [#què-fer] Deixa un únic cursor: `starting_after` per avançar per la col·lecció, `ending_before` per retrocedir. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # corrective_invoice_inanulable (/ca/errors/corrective_invoice_inanulable) | Code | Type | HTTP | Categoria | | ------------------------------- | ----------------------- | ---- | ------------------------------------- | | `corrective_invoice_inanulable` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La factura és al seu torn una rectificativa, i les rectificatives no s'anul·len mai: la cadena de correcció ha de seguir sent auditable de punta a punta. ## Què fer [#què-fer] Emet una rectificativa nova contra la factura original, amb els imports correctes. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # custom_header_blocklisted (/ca/errors/custom_header_blocklisted) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `custom_header_blocklisted` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] Una de les capçaleres personalitzades està reservada: la gestiona la capa HTTP (`host`, `content-type`, `content-length`, `user-agent`), l'envia Factuarea com a part del contracte signat (`factuarea-*`), o pertany al proxy (`x-forwarded-*`). ## Què fer [#què-fer] Reanomena la capçalera —`x-la-meva-app-token` en lloc d'una de reservada— o treu-la si la plataforma ja envia aquesta informació. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # custom_header_value_too_long (/ca/errors/custom_header_value_too_long) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `custom_header_value_too_long` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] El valor d'una capçalera personalitzada supera els 1024 caràcters. ## Què fer [#què-fer] Envia un testimoni o una referència curta en lloc del contingut complet; les dades van al cos de l'esdeveniment. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # custom_tax_creation_disabled (/ca/errors/custom_tax_creation_disabled) | Code | Type | HTTP | Categoria | | ------------------------------ | --------------------- | ---- | ---------------------------------- | | `custom_tax_creation_disabled` | `authorization_error` | 403 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] La creació d'impostos personalitzats està deshabilitada per a aquesta empresa. ## Què fer [#què-fer] Fes servir un impost del catàleg canònic i fixa les teves preferències mitjançant els defaults fiscals de l'empresa. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # declaracion_already_exists (/ca/errors/declaracion_already_exists) | Code | Type | HTTP | Categoria | | ---------------------------- | ---------------- | ---- | --------------------------------------- | | `declaracion_already_exists` | `conflict_error` | 409 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'empresa ja té presentada la declaració responsable del SIF d'aquest període. ## Què fer [#què-fer] Descarrega la declaració existent en lloc de generar-ne una de nova. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # declaracion_not_found (/ca/errors/declaracion_not_found) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------- | ---- | --------------------------------------- | | `declaracion_not_found` | `not_found_error` | 404 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'empresa no té presentada la declaració responsable del SIF del període sol·licitat. ## Què fer [#què-fer] Genera la declaració abans de descarregar-la o consultar-la. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # delivery_note_not_found (/ca/errors/delivery_note_not_found) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------- | ---- | ------------------------------------------- | | `delivery_note_not_found` | `not_found_error` | 404 | [Albarans](/ca/errors/index-delivery-notes) | ## Causa [#causa] L'identificador no resol a cap albarà de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu, o localitza l'albarà pel seu `external_id`. ## Relacionat [#relacionat] * [Tots els codis d'error d'Albarans](/ca/errors/index-delivery-notes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # delivery_note_section_not_editable_in_status (/ca/errors/delivery_note_section_not_editable_in_status) | Code | Type | HTTP | Categoria | | ---------------------------------------------- | ----------------------- | ---- | ------------------------------------------- | | `delivery_note_section_not_editable_in_status` | `invalid_request_error` | 422 | [Albarans](/ca/errors/index-delivery-notes) | ## Causa [#causa] La secció logística —transportista, vehicle, conductor— està congelada perquè l'albarà ja està lliurat, facturat o cancel·lat. ## Què fer [#què-fer] Registra la correcció a la factura que cobra el lliurament, o emet un albarà nou si la mercaderia torna a viatjar. ## Relacionat [#relacionat] * [Tots els codis d'error d'Albarans](/ca/errors/index-delivery-notes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # dependency_unavailable (/ca/errors/dependency_unavailable) | Code | Type | HTTP | Categoria | | ------------------------ | --------------------------- | ---- | ----------------------------------- | | `dependency_unavailable` | `service_unavailable_error` | 503 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] Un servei extern del qual depèn l'operació no va respondre a temps. ## Què fer [#què-fer] Reintenta després d'una espera breu; si l'operació és d'escriptura, reutilitza la mateixa `Idempotency-Key` perquè el reintent no la dupliqui. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # direct_debit_requires_default_bank_account (/ca/errors/direct_debit_requires_default_bank_account) | Code | Type | HTTP | Categoria | | -------------------------------------------- | ----------------------- | ---- | ----------------------------------- | | `direct_debit_requires_default_bank_account` | `invalid_request_error` | 422 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] Es va triar domiciliació bancària com a mètode de pagament, però el client no té compte bancari per defecte on carregar. ## Què fer [#què-fer] Afegeix un compte bancari al client i marca'l com a predeterminat; després fixa el mètode de pagament. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # document_type_required_for_ambiguous_code (/ca/errors/document_type_required_for_ambiguous_code) | Code | Type | HTTP | Categoria | | ------------------------------------------- | ----------------------- | ---- | --------------------------------- | | `document_type_required_for_ambiguous_code` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] Aquest codi de sèrie existeix per a més d'un tipus de document, així que per si sol no identifica una única sèrie. ## Què fer [#què-fer] Repeteix la cerca afegint el tipus de document al costat del codi. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # driver_tax_id_requires_name (/ca/errors/driver_tax_id_requires_name) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ------------------------------------------- | | `driver_tax_id_requires_name` | `invalid_request_error` | 422 | [Albarans](/ca/errors/index-delivery-notes) | ## Causa [#causa] Es va enviar el NIF del conductor sense el seu nom, i un identificador sense nom no identifica ningú al document de lliurament. ## Què fer [#què-fer] Envia `driver_name` juntament amb `driver_tax_id`, o deixa'ls tots dos fora. ## Relacionat [#relacionat] * [Tots els codis d'error d'Albarans](/ca/errors/index-delivery-notes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # duplicate_tax_default_for_document_type (/ca/errors/duplicate_tax_default_for_document_type) | Code | Type | HTTP | Categoria | | ----------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `duplicate_tax_default_for_document_type` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] Ja hi ha un altre impost del mateix tipus marcat com a default per a aquest tipus de document, i el parell (tipus d'impost, tipus de document) admet un únic default. ## Què fer [#què-fer] Treu el default a l'impost que l'ocupa, o marca el nou default sobre un altre tipus de document. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # employee_seat_charge_failed (/ca/errors/employee_seat_charge_failed) | Code | Type | HTTP | Categoria | | ----------------------------- | ------------------------ | ---- | -------------------------------------- | | `employee_seat_charge_failed` | `payment_required_error` | 402 | [Empleats](/ca/errors/index-employees) | ## Causa [#causa] El cobrament immediat del prorrateig del seient d'empleat va ser rebutjat: la targeta es va denegar, necessita autenticació, o el proveïdor de pagament era inaccessible. L'empleat no s'activa si el seient no es cobra. ## Què fer [#què-fer] Arregla el mètode de pagament al portal de facturació i reintenta; consulta amb el teu banc si la targeta es continua denegant. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empleats](/ca/errors/index-employees) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # employee_seat_payment_method_required (/ca/errors/employee_seat_payment_method_required) | Code | Type | HTTP | Categoria | | --------------------------------------- | ------------------------ | ---- | -------------------------------------- | | `employee_seat_payment_method_required` | `payment_required_error` | 402 | [Empleats](/ca/errors/index-employees) | ## Causa [#causa] Donar d'alta o reactivar un empleat cobra un seient immediatament, i l'empresa opera en mode real sense mètode de pagament configurat. ## Què fer [#què-fer] Obre el portal de facturació a `error.details.payment_setup_url`, registra un mètode de pagament i repeteix la mateixa crida. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empleats](/ca/errors/index-employees) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # event_already_processed (/ca/errors/event_already_processed) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | --------------------------------------- | | `event_already_processed` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] Aquest esdeveniment del SIF ja consta a la cadena d'esdeveniments, i cada esdeveniment es processa exactament una vegada. ## Què fer [#què-fer] No tornis a enviar l'esdeveniment; l'entrada existent ja el cobreix. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # event_not_found (/ca/errors/event_not_found) | Code | Type | HTTP | Categoria | | ----------------- | ----------------- | ---- | --------------------------------- | | `event_not_found` | `not_found_error` | 404 | [Events](/ca/errors/index-events) | ## Causa [#causa] L'identificador no correspon a cap esdeveniment de l'empresa autenticada, o l'esdeveniment va ser purgat per la política de retenció de 30 dies. ## Què fer [#què-fer] Llegeix l'estat actual des del recurs a què es referia l'esdeveniment; el feed d'esdeveniments és una finestra recent, no un arxiu permanent. ## Relacionat [#relacionat] * [Tots els codis d'error d'Events](/ca/errors/index-events) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # export_limit_exceeded (/ca/errors/export_limit_exceeded) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ------------------------------------- | | `export_limit_exceeded` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La selecció filtrada supera el límit de 5.000 factures de l'exportació, així que el fitxer es rebutja d'entrada en lloc de truncar-se en silenci. ## Què fer [#què-fer] Acota els filtres — per rang de dates o per sèrie — i exporta les factures en diversos lots. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # external_id_already_exists (/ca/errors/external_id_already_exists) | Code | Type | HTTP | Categoria | | ---------------------------- | ---------------- | ---- | ----------------------------------- | | `external_id_already_exists` | `conflict_error` | 409 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'`external_id` amb què concilies contra el teu sistema ja està assignat a un altre objecte del mateix tipus en aquesta empresa. ## Què fer [#què-fer] Localitza l'objecte pel seu `external_id` i actualitza'l, o assigna-li un altre valor: `external_id` és únic per tipus de recurs i empresa. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # face_transmission_failed (/ca/errors/face_transmission_failed) | Code | Type | HTTP | Categoria | | -------------------------- | ----------- | ---- | ----------------------------------- | | `face_transmission_failed` | `api_error` | 502 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] La plataforma FACe —el punt d'entrada de les administracions públiques— era inaccessible o va respondre amb una fallada. El problema és aigües amunt, no a la teva petició. ## Què fer [#què-fer] Reintenta més tard; la factura conserva el seu estat i es pot tornar a presentar sense reemetre-la. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # facturae_signing_failed (/ca/errors/facturae_signing_failed) | Code | Type | HTTP | Categoria | | ------------------------- | ----------- | ---- | ----------------------------------- | | `facturae_signing_failed` | `api_error` | 500 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] No es va poder produir la signatura XAdES del fitxer Facturae, normalment perquè el certificat de signatura no és utilitzable en aquell moment. ## Què fer [#què-fer] Comprova que el certificat de l'empresa és vàlid i coincideix amb el seu NIF; un cop arreglat, torna a generar el fitxer. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # feature_not_available_in_plan (/ca/errors/feature_not_available_in_plan) | Code | Type | HTTP | Categoria | | ------------------------------- | --------------------- | ---- | ---------------------------------------------- | | `feature_not_available_in_plan` | `authorization_error` | 403 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] La funcionalitat no està inclosa en el pla de l'empresa. ## Què fer [#què-fer] Puja a un pla que la inclogui, o fes servir la funcionalitat equivalent que sí que ofereix el teu pla actual. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # forbidden_action (/ca/errors/forbidden_action) | Code | Type | HTTP | Categoria | | ------------------ | --------------------- | ---- | ---------------------------------------------- | | `forbidden_action` | `authorization_error` | 403 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] L'acció està bloquejada per a aquest recurs encara que l'abast sigui el correcte: el recurs pertany a un catàleg compartit, o el canvi va per un altre endpoint. ## Què fer [#què-fer] Llegeix `error.subcode` i `error.message`: indiquen la via canònica per al que estàs intentant fer. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # gestoria_module_required (/ca/errors/gestoria_module_required) | Code | Type | HTTP | Categoria | | -------------------------- | --------------------- | ---- | -------------------------------------- | | `gestoria_module_required` | `authorization_error` | 403 | [Empreses](/ca/errors/index-companies) | ## Causa [#causa] La gestoria té un pla vigent, però sense el mòdul de gestoria, així que no pot crear ni operar empreses gestionades. ## Què fer [#què-fer] Puja a un pla que inclogui el mòdul; això és un límit de pla, no un pagament pendent. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empreses](/ca/errors/index-companies) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # gestoria_plan_required (/ca/errors/gestoria_plan_required) | Code | Type | HTTP | Categoria | | ------------------------ | ------------------------ | ---- | -------------------------------------- | | `gestoria_plan_required` | `payment_required_error` | 402 | [Empreses](/ca/errors/index-companies) | ## Causa [#causa] La gestoria no té una subscripció de pagament activa, així que no hi ha subscripció sobre la qual cobrar el seient. ## Què fer [#què-fer] Contracta un pla, o reprèn el que va cancel·lar, abans d'afegir empreses gestionades. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empreses](/ca/errors/index-companies) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # idempotency_key_in_use (/ca/errors/idempotency_key_in_use) | Code | Type | HTTP | Categoria | | ------------------------ | ------------------- | ---- | ------------------------------------------- | | `idempotency_key_in_use` | `idempotency_error` | 409 | [Idempotency](/ca/errors/index-idempotency) | ## Causa [#causa] Hi ha una altra petició amb la mateixa `Idempotency-Key` encara en curs, i encara no se'n coneix el resultat. ## Què fer [#què-fer] Espera que respongui la primera petició i llegeix-ne la resposta; reintenta amb la mateixa clau després d'una espera breu si es va tallar la connexió. ## Relacionat [#relacionat] * [Tots els codis d'error d'Idempotency](/ca/errors/index-idempotency) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # idempotency_key_invalid (/ca/errors/idempotency_key_invalid) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ------------------------------------------- | | `idempotency_key_invalid` | `invalid_request_error` | 400 | [Idempotency](/ca/errors/index-idempotency) | ## Causa [#causa] La `Idempotency-Key` no encaixa amb el format admès: entre 1 i 255 caràcters ASCII imprimibles. ## Què fer [#què-fer] Genera la clau com un UUID o una cadena aleatòria, i mantén-la estable entre els reintents d'una mateixa operació. ## Relacionat [#relacionat] * [Tots els codis d'error d'Idempotency](/ca/errors/index-idempotency) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # idempotency_key_reused (/ca/errors/idempotency_key_reused) | Code | Type | HTTP | Categoria | | ------------------------ | ------------------- | ---- | ------------------------------------------- | | `idempotency_key_reused` | `idempotency_error` | 409 | [Idempotency](/ca/errors/index-idempotency) | ## Causa [#causa] Aquesta `Idempotency-Key` ja es va fer servir amb un payload diferent. La clau identifica una operació concreta, així que reutilitzar-la per a una altra buidaria de sentit el replay. ## Què fer [#què-fer] Fes servir una clau nova per a cada operació diferent, i reutilitza una clau només per reintentar exactament la mateixa petició. ## Relacionat [#relacionat] * [Tots els codis d'error d'Idempotency](/ca/errors/index-idempotency) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Compte (/ca/errors/index-account) Codis d'error que emet Compte. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------- | | [`account_not_found`](/ca/errors/account_not_found) | `not_found_error` | 404 | No es va poder resoldre el compte associat a la clau, cosa que sol voler dir que la clau ja no apunta a una empresa viva. | | [`api_key_already_revoked`](/ca/errors/api_key_already_revoked) | `invalid_request_error` | 422 | La clau ja estava revocada, i una clau revocada no admet més operacions: la revocació és terminal. | | [`api_key_not_found`](/ca/errors/api_key_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap clau API de l'empresa autenticada. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Autenticació (/ca/errors/index-authentication) Codis d'error que emet Autenticació. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ---------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`api_key_expired`](/ca/errors/api_key_expired) | `authentication_error` | 401 | La clau va passar la seva data de caducitat. | | [`api_key_revoked`](/ca/errors/api_key_revoked) | `authentication_error` | 401 | La clau va ser revocada, i una clau revocada no torna a autenticar mai: revocar és justament la manera de tallar una credencial filtrada. | | [`invalid_api_key`](/ca/errors/invalid_api_key) | `authentication_error` | 401 | La clau no correspon a cap clau activa. Pot estar mal copiada, truncada, o pertànyer a un altre entorn: les claus de prova i les de producció no són intercanviables. | | [`ip_not_allowed`](/ca/errors/ip_not_allowed) | `authentication_error` | 401 | La clau restringeix les adreces que accepta, i la petició va arribar des d'una que no és a la llista. | | [`missing_api_key`](/ca/errors/missing_api_key) | `authentication_error` | 401 | La petició no porta credencials: ni capçalera `Authorization` ni `X-API-Key`. | | [`origin_not_allowed`](/ca/errors/origin_not_allowed) | `authentication_error` | 401 | La petició ve d'un origen de navegador que la clau no accepta. | | [`too_many_auth_failures`](/ca/errors/too_many_auth_failures) | `authentication_error` | 429 | Van arribar massa intents fallits d'autenticació des de la mateixa adreça, així que queda bloquejada temporalment per frenar els intents d'endevinar credencials. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Autorització (/ca/errors/index-authorization) Codis d'error que emet Autorització. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------- | --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_not_active`](/ca/errors/addon_not_active) | `authorization_error` | 403 | La funcionalitat pertany a un add-on que ara mateix no està actiu per a l'empresa. | | [`feature_not_available_in_plan`](/ca/errors/feature_not_available_in_plan) | `authorization_error` | 403 | La funcionalitat no està inclosa en el pla de l'empresa. | | [`forbidden_action`](/ca/errors/forbidden_action) | `authorization_error` | 403 | L'acció està bloquejada per a aquest recurs encara que l'abast sigui el correcte: el recurs pertany a un catàleg compartit, o el canvi va per un altre endpoint. | | [`insufficient_scope`](/ca/errors/insufficient_scope) | `authorization_error` | 403 | La clau autentica correctament però no porta l'abast que exigeix aquesta operació. Els abasts es concedeixen en emetre la clau i no s'amplien en temps de crida. | | [`max_api_keys_exceeded`](/ca/errors/max_api_keys_exceeded) | `authorization_error` | 422 | L'empresa va arribar al nombre de claus API que permet el seu pla. | | [`max_webhook_endpoints_exceeded`](/ca/errors/max_webhook_endpoints_exceeded) | `authorization_error` | 422 | L'empresa va arribar al nombre d'endpoints de webhook que permet el seu nivell d'add-on. | | [`module_not_available_in_sandbox`](/ca/errors/module_not_available_in_sandbox) | `authorization_error` | 403 | El recurs pertany a un mòdul vetat en mode test. La sandbox mai toca l'AEAT, els bancs ni cobraments reals, així que aquests mòduls queden fora a propòsit. | | [`scope_not_allowed_by_plan`](/ca/errors/scope_not_allowed_by_plan) | `authorization_error` | 422 | Un dels abasts demanats pertany a un mòdul que el pla no inclou, així que la clau naixeria amb un permís que mai podria exercir. | | [`scope_not_allowed_in_sandbox`](/ca/errors/scope_not_allowed_in_sandbox) | `authorization_error` | 422 | Una clau de prova no pot néixer amb abasts de mòduls vetats a la sandbox. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Clients (/ca/errors/index-clients) Codis d'error que emet Clients. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`alternative_id_type_invalid`](/ca/errors/alternative_id_type_invalid) | `invalid_request_error` | 422 | El tipus d'identificador alternatiu queda fora del catàleg `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. | | [`cannot_have_both_tax_id_and_alternative_id`](/ca/errors/cannot_have_both_tax_id_and_alternative_id) | `invalid_request_error` | 422 | El client envia `tax_id` i un identificador alternatiu alhora. La identitat fiscal és una: l'identificador alternatiu existeix precisament per a parts sense NIF espanyol. | | [`census_requires_tax_id`](/ca/errors/census_requires_tax_id) | `invalid_request_error` | 422 | La verificació censal contrasta el parell nom + NIF contra l'AEAT, i en falta un dels dos. | | [`client_has_documents`](/ca/errors/client_has_documents) | `invalid_request_error` | 422 | El client està referenciat per documents emesos. Esborrar-lo deixaria factures, pressupostos o albarans sense la part a qui es van emetre, i els registres fiscals han de seguir sent traçables. | | [`client_import_too_large`](/ca/errors/client_import_too_large) | `invalid_request_error` | 422 | El CSV supera el límit de files que admet la importació síncrona, ja que el fitxer sencer es processa dins de la mateixa petició. | | [`client_not_found`](/ca/errors/client_not_found) | `not_found_error` | 404 | L'identificador no resol a cap client de l'empresa autenticada. | | [`client_requires_tax_identity`](/ca/errors/client_requires_tax_identity) | `invalid_request_error` | 422 | El client no té identitat fiscal: ni `tax_id` ni identificador alternatiu, i no es pot emetre una factura a una part sense identificar. | | [`direct_debit_requires_default_bank_account`](/ca/errors/direct_debit_requires_default_bank_account) | `invalid_request_error` | 422 | Es va triar domiciliació bancària com a mètode de pagament, però el client no té compte bancari per defecte on carregar. | | [`tax_id_already_exists`](/ca/errors/tax_id_already_exists) | `conflict_error` | 409 | Un altre client de l'empresa ja té aquest NIF, i el NIF identifica la part sense ambigüitat dins d'una empresa. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Empreses (/ca/errors/index-companies) Codis d'error que emet Empreses. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`company_inactive`](/ca/errors/company_inactive) | `authorization_error` | 403 | El perfil que indica `X-Active-Profile` és una de les teves empreses gestionades, però està desactivada i no es pot operar fins que torni a estar activa. | | [`gestoria_module_required`](/ca/errors/gestoria_module_required) | `authorization_error` | 403 | La gestoria té un pla vigent, però sense el mòdul de gestoria, així que no pot crear ni operar empreses gestionades. | | [`gestoria_plan_required`](/ca/errors/gestoria_plan_required) | `payment_required_error` | 402 | La gestoria no té una subscripció de pagament activa, així que no hi ha subscripció sobre la qual cobrar el seient. | | [`payment_method_required`](/ca/errors/payment_method_required) | `payment_required_error` | 402 | Donar d'alta una empresa gestionada cobra un seient immediatament, i la gestoria opera en mode real sense mètode de pagament configurat. | | [`seat_charge_failed`](/ca/errors/seat_charge_failed) | `payment_required_error` | 402 | El cobrament immediat del prorrateig del seient va ser rebutjat: la targeta es va denegar, necessita autenticació, o el proveïdor de pagament era inaccessible. L'empresa no es crea si el seient no es cobra. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Albarans (/ca/errors/index-delivery-notes) Codis d'error que emet Albarans. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------- | | [`delivery_note_not_found`](/ca/errors/delivery_note_not_found) | `not_found_error` | 404 | L'identificador no resol a cap albarà de l'empresa autenticada. | | [`delivery_note_section_not_editable_in_status`](/ca/errors/delivery_note_section_not_editable_in_status) | `invalid_request_error` | 422 | La secció logística —transportista, vehicle, conductor— està congelada perquè l'albarà ja està lliurat, facturat o cancel·lat. | | [`driver_tax_id_requires_name`](/ca/errors/driver_tax_id_requires_name) | `invalid_request_error` | 422 | Es va enviar el NIF del conductor sense el seu nom, i un identificador sense nom no identifica ningú al document de lliurament. | | [`signature_payload_too_large`](/ca/errors/signature_payload_too_large) | `invalid_request_error` | 422 | La imatge de la signatura supera la mida admesa per al camp. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Empleats (/ca/errors/index-employees) Codis d'error que emet Empleats. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------------------- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`employee_seat_charge_failed`](/ca/errors/employee_seat_charge_failed) | `payment_required_error` | 402 | El cobrament immediat del prorrateig del seient d'empleat va ser rebutjat: la targeta es va denegar, necessita autenticació, o el proveïdor de pagament era inaccessible. L'empleat no s'activa si el seient no es cobra. | | [`employee_seat_payment_method_required`](/ca/errors/employee_seat_payment_method_required) | `payment_required_error` | 402 | Donar d'alta o reactivar un empleat cobra un seient immediatament, i l'empresa opera en mode real sense mètode de pagament configurat. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Events (/ca/errors/index-events) Codis d'error que emet Events. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ----------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | [`event_not_found`](/ca/errors/event_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap esdeveniment de l'empresa autenticada, o l'esdeveniment va ser purgat per la política de retenció de 30 dies. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Idempotency (/ca/errors/index-idempotency) Codis d'error que emet Idempotency. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`idempotency_key_in_use`](/ca/errors/idempotency_key_in_use) | `idempotency_error` | 409 | Hi ha una altra petició amb la mateixa `Idempotency-Key` encara en curs, i encara no se'n coneix el resultat. | | [`idempotency_key_invalid`](/ca/errors/idempotency_key_invalid) | `invalid_request_error` | 400 | La `Idempotency-Key` no encaixa amb el format admès: entre 1 i 255 caràcters ASCII imprimibles. | | [`idempotency_key_reused`](/ca/errors/idempotency_key_reused) | `idempotency_error` | 409 | Aquesta `Idempotency-Key` ja es va fer servir amb un payload diferent. La clau identifica una operació concreta, així que reutilitzar-la per a una altra buidaria de sentit el replay. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Factures (/ca/errors/index-invoices) Codis d'error que emet Factures. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`corrective_invoice_inanulable`](/ca/errors/corrective_invoice_inanulable) | `invalid_request_error` | 422 | La factura és al seu torn una rectificativa, i les rectificatives no s'anul·len mai: la cadena de correcció ha de seguir sent auditable de punta a punta. | | [`export_limit_exceeded`](/ca/errors/export_limit_exceeded) | `invalid_request_error` | 422 | La selecció filtrada supera el límit de 5.000 factures de l'exportació, així que el fitxer es rebutja d'entrada en lloc de truncar-se en silenci. | | [`invalid_correction_nature`](/ca/errors/invalid_correction_nature) | `invalid_request_error` | 422 | `correction_nature` només accepta `S` (substitució: la rectificativa porta els imports corregits complets) o `I` (per diferències: només porta el delta). | | [`invalid_correction_reason`](/ca/errors/invalid_correction_reason) | `invalid_request_error` | 422 | El motiu de rectificació queda fora de la llista fiscal tancada (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), que mapeja als codis AEAT R1 a R4. | | [`invalid_invoice_id`](/ca/errors/invalid_invoice_id) | `invalid_request_error` | 400 | La referència de factura rebuda no és un identificador vàlid; sol voler dir que s'ha colat un valor intern on l'API espera l'`id` públic. | | [`invalid_invoice_number`](/ca/errors/invalid_invoice_number) | `invalid_request_error` | 422 | El número de factura no segueix el format canònic `SÈRIE-AAAA-NNN`, més el sufix `-RECn` a les rectificatives. | | [`invalid_invoice_status`](/ca/errors/invalid_invoice_status) | `invalid_request_error` | 422 | El valor enviat com a estat de factura queda fora del catàleg del cicle de vida (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). | | [`invalid_invoice_uuid`](/ca/errors/invalid_invoice_uuid) | `invalid_request_error` | 400 | L'identificador de factura de la ruta o del payload no és un UUID vàlid. | | [`invalid_payment_method`](/ca/errors/invalid_payment_method) | `invalid_request_error` | 422 | El mètode de pagament queda fora de l'allowlist tancada: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. | | [`invoice_already_annulled`](/ca/errors/invoice_already_annulled) | `invalid_request_error` | 422 | La factura ja estava anul·lada. L'anul·lació és terminal i, amb VeriFactu actiu, el seu registre d'anul·lació ja va arribar a l'AEAT. | | [`invoice_already_paid`](/ca/errors/invoice_already_paid) | `invalid_request_error` | 422 | La factura ja està cobrada. `paid` és un estat terminal i comptablement tancat: l'IVA repercutit ja s'ha declarat, o es declararà en el període. | | [`invoice_already_sent`](/ca/errors/invoice_already_sent) | `invalid_request_error` | 422 | La factura ja va ser emesa: té número definitiu de sèrie i, amb VeriFactu actiu, l'alta a l'AEAT. L'emissió no passa dues vegades. | | [`invoice_cannot_assign_number`](/ca/errors/invoice_cannot_assign_number) | `invalid_request_error` | 422 | Es va demanar número definitiu per a una factura que no és esborrany, o que ja en té. La numeració de sèrie és monòtona i els números no es reassignen. | | [`invoice_invalid_status_transition`](/ca/errors/invoice_invalid_status_transition) | `invalid_request_error` | 422 | L'estat destí no és assolible des de l'actual. El cicle de vida és dirigit: `draft` passa a `scheduled` o `sent`, `sent` a `paid`, `overdue` o `annulled`, i `paid`, `cancelled` i `annulled` són terminals. | | [`invoice_not_cancellable_in_current_state`](/ca/errors/invoice_not_cancellable_in_current_state) | `invalid_request_error` | 422 | Cancel·lar retira un esborrany que encara no és fiscalment vinculant, així que només s'aplica mentre la factura està en `draft`. | | [`invoice_not_correctable_in_current_state`](/ca/errors/invoice_not_correctable_in_current_state) | `invalid_request_error` | 422 | Una rectificativa només s'emet contra una factura ja emesa (`sent` o `paid`). Un esborrany, una factura cancel·lada o una anul·lada no tenen res a rectificar. | | [`invoice_not_deletable_in_current_state`](/ca/errors/invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Només s'esborren les factures en `draft` i `cancelled`. Una factura numerada no desapareix mai: la sèrie correlativa ha de seguir sent auditable. | | [`invoice_not_editable_in_current_state`](/ca/errors/invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Només un esborrany admet edició. Un cop emesa, la factura és immutable i el seu contingut queda congelat juntament amb el seu registre fiscal. | | [`invoice_not_eligible_for_action`](/ca/errors/invoice_not_eligible_for_action) | `invalid_request_error` | 422 | L'acció sol·licitada no s'aplica a aquesta factura: el seu tipus o el seu estat actual la deixen fora de l'abast de l'operació. | | [`invoice_not_found`](/ca/errors/invoice_not_found) | `not_found_error` | 404 | L'identificador no resol a cap factura de l'empresa autenticada. Les factures d'una altra empresa responen exactament igual. | | [`invoice_not_modifiable_in_current_state`](/ca/errors/invoice_not_modifiable_in_current_state) | `invalid_request_error` | 422 | El camp que intentes canviar està congelat per a l'estat actual — per exemple el règim fiscal d'una factura anul·lada. | | [`invoice_not_paid`](/ca/errors/invoice_not_paid) | `invalid_request_error` | 422 | Es va demanar un justificant de pagament d'una factura sense cobrament registrat, així que no hi ha res a certificar. | | [`invoice_not_reschedulable_in_current_state`](/ca/errors/invoice_not_reschedulable_in_current_state) | `invalid_request_error` | 422 | Reprogramar mou la data d'emissió d'una factura que espera en `scheduled`, i aquesta factura no està esperant. | | [`invoice_not_schedulable_in_current_state`](/ca/errors/invoice_not_schedulable_in_current_state) | `invalid_request_error` | 422 | Només un esborrany es pot programar: la programació reserva un moment futur d'emissió sense consumir encara número de sèrie. | | [`invoice_not_unschedulable_in_current_state`](/ca/errors/invoice_not_unschedulable_in_current_state) | `invalid_request_error` | 422 | Desprogramar torna la factura de `scheduled` a `draft`, així que només s'aplica mentre segueix esperant a emetre's. | | [`invoice_not_unsendable_in_current_state`](/ca/errors/invoice_not_unsendable_in_current_state) | `invalid_request_error` | 422 | Desfer la marca de lliurament només s'aplica a una factura `sent`: neteja `sent_at` i manté la factura emesa. | | [`invoice_requires_at_least_one_line`](/ca/errors/invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura no porta cap línia d'operació, així que no té base imposable i no es pot emetre. Passa tant quan no envies línies com quan totes les que envies són de suplert: un suplert és una quantitat pagada per compte del client (art. 78.Tres.3 LIVA), no una operació teva. | | [`invoice_year_required_for_ambiguous_number`](/ca/errors/invoice_year_required_for_ambiguous_number) | `invalid_request_error` | 422 | Aquest número de factura existeix en més d'un exercici, així que per si sol no identifica una única factura. | | [`line_total_checksum_mismatch`](/ca/errors/line_total_checksum_mismatch) | `invalid_request_error` | 422 | El `line_total` declarat no coincideix amb el que calcula Factuarea per a aquella línia (quantitat × preu − descompte + IVA − retenció + recàrrec) i la desviació supera el cèntim de tolerància. L'import que es factura i es declara a l'AEAT és sempre el calculat aquí, així que la discrepància vol dir que el teu sistema i la factura emesa no quadrarien. | | [`line_type_invalid`](/ca/errors/line_type_invalid) | `invalid_request_error` | 422 | El tipus de línia queda fora del catàleg tancat `NORMAL` / `SUPLIDO`. Una factura emesa només distingeix dues naturaleses: el que véns tu, que forma base imposable i porta IVA, i el suplert, que són diners avançats en nom i per compte del client i per això queda fora de la base (art. 78.Tres.3 LIVA). | | [`no_invoices_in_period`](/ca/errors/no_invoices_in_period) | `invalid_request_error` | 422 | L'operació trimestral no va trobar factures en el període demanat, així que no hi ha res a empaquetar ni a enviar. | | [`payment_method_invalid`](/ca/errors/payment_method_invalid) | `invalid_request_error` | 422 | La mateixa allowlist tancada que `invalid_payment_method`, reportada quan el valor es rebutja en llegir el camp de mètode de pagament del payload. | | [`reminder_not_applicable`](/ca/errors/reminder_not_applicable) | `invalid_request_error` | 422 | El recordatori de pagament no escau: la factura no està en `sent` ni `overdue`, no hi ha adreça de destinatari, falta l'enllaç públic o està desactivat, o ja va sortir un altre recordatori les últimes 24 hores. | | [`scheduled_for_in_past`](/ca/errors/scheduled_for_in_past) | `invalid_request_error` | 422 | `scheduled_for` no és estrictament futur, així que no hi ha cap espera a reservar. | | [`simplified_invoice_cannot_be_substituted`](/ca/errors/simplified_invoice_cannot_be_substituted) | `invalid_request_error` | 422 | Una de les factures de la llista de substitució no es pot substituir: no és simplificada, està cancel·lada o anul·lada, pertany a una altra empresa, o ja té substitutiva. | | [`simplified_invoice_not_allowed`](/ca/errors/simplified_invoice_not_allowed) | `invalid_request_error` | 422 | L'operació no és elegible per a factura simplificada: supera els 3.000 €, o és un lliurament intracomunitari, una exportació, una operació amb inversió del subjecte passiu, o el client necessita factura completa per deduir l'IVA. | | [`simplified_limit_exceeded`](/ca/errors/simplified_limit_exceeded) | `invalid_request_error` | 422 | Les línies portarien la factura simplificada (F2) per sobre del límit legal absolut de 3.000 € IVA inclòs. | | [`suplido_line_cannot_carry_taxes`](/ca/errors/suplido_line_cannot_carry_taxes) | `invalid_request_error` | 422 | La línia de suplert porta càrrega pròpia: tipus d'IVA, retenció, recàrrec d'equivalència, descompte, clau de règim, causa d'exempció o producte/paquet. Un suplert no és una operació de l'emissor, així que repercutir-hi un impost seria tributar per un lliurament que no has fet, i lligar-lo a un producte mouria un estoc que mai no has venut. | | [`suplido_not_allowed_in_simplified_invoice`](/ca/errors/suplido_not_allowed_in_simplified_invoice) | `invalid_request_error` | 422 | La factura és simplificada (F2) i una simplificada no identifica el destinatari. Sense destinatari identificat no hi ha a qui acreditar el pagament per compte d'altri, així que l'import no admet el tractament de suplert en aquest tipus de factura. | | [`suplido_requires_source_invoice_reference`](/ca/errors/suplido_requires_source_invoice_reference) | `invalid_request_error` | 422 | La línia de suplert no informa `source_invoice_reference`, el número del justificant que el tercer va expedir a nom del client. Sense aquest justificant el pagament no s'acredita com a fet per compte d'altri i Hisenda el tractaria com a base imposable pròpia de l'emissor, amb el seu IVA repercutit. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Notificacions (/ca/errors/index-notifications) Codis d'error que emet Notificacions. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------ | | [`notification_not_found`](/ca/errors/notification_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap notificació de l'empresa autenticada, o la notificació va quedar fora de la finestra de retenció. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Pagaments (/ca/errors/index-payments) Codis d'error que emet Pagaments. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_payment_date`](/ca/errors/invalid_payment_date) | `invalid_request_error` | 422 | La data de pagament queda fora de la finestra admesa: no pot ser anterior a la data d'emissió de la factura ni situar-se al futur. | | [`payout_reconciliation_amount_mismatch`](/ca/errors/payout_reconciliation_amount_mismatch) | `invalid_request_error` | 422 | L'import confirmat no coincideix amb el net de la liquidació, així que la conciliació tancaria amb una diferència que ningú justifica. | | [`receipt_not_available`](/ca/errors/receipt_not_available) | `invalid_request_error` | 422 | No hi ha justificant a emetre perquè el document no té cap cobrament registrat al darrere. | | [`stripe_payout_already_reconciled`](/ca/errors/stripe_payout_already_reconciled) | `invalid_request_error` | 422 | La liquidació ja estava conciliada, i la conciliació és terminal: repetir-la comptabilitzaria dues vegades l'apunt bancari. | | [`stripe_payout_not_found`](/ca/errors/stripe_payout_not_found) | `not_found_error` | 404 | L'identificador no resol a cap liquidació de l'empresa autenticada. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Productes (/ca/errors/index-products) Codis d'error que emet Productes. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | | [`pack_in_use`](/ca/errors/pack_in_use) | `invalid_request_error` | 422 | El pack està referenciat per documents emesos, així que esborrar-lo trencaria la seva composició. | | [`pack_not_found`](/ca/errors/pack_not_found) | `not_found_error` | 404 | L'identificador no resol a cap pack de l'empresa autenticada. | | [`pack_share_link_failed`](/ca/errors/pack_share_link_failed) | `api_error` | 500 | No es va poder generar l'enllaç per compartir el pack. El pack en si no queda afectat. | | [`product_in_use`](/ca/errors/product_in_use) | `invalid_request_error` | 422 | El producte està referenciat per documents emesos o per altres entrades del catàleg, i eliminar-lo deixaria aquestes referències penjant. | | [`product_not_found`](/ca/errors/product_not_found) | `not_found_error` | 404 | L'identificador no resol a cap producte de l'empresa autenticada. | | [`sku_already_exists`](/ca/errors/sku_already_exists) | `conflict_error` | 409 | Un altre producte de l'empresa ja fa servir aquest SKU, i el SKU identifica l'article sense ambigüitat dins del catàleg. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Factures proforma (/ca/errors/index-proformas) Codis d'error que emet Factures proforma. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`invalid_expiry_date`](/ca/errors/invalid_expiry_date) | `invalid_request_error` | 422 | La data de venciment és anterior a la d'emissió, o la supera en més de 365 dies. | | [`invalid_proforma_id`](/ca/errors/invalid_proforma_id) | `invalid_request_error` | 400 | La referència de proforma rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. | | [`invalid_proforma_number`](/ca/errors/invalid_proforma_number) | `invalid_request_error` | 422 | El número de proforma no segueix el format canònic de numeració de la seva sèrie. | | [`invalid_proforma_status`](/ca/errors/invalid_proforma_status) | `invalid_request_error` | 422 | El valor enviat com a estat queda fora del catàleg `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. | | [`invalid_proforma_uuid`](/ca/errors/invalid_proforma_uuid) | `invalid_request_error` | 400 | L'identificador de proforma de la ruta o del payload no és un UUID vàlid. | | [`proforma_already_accepted`](/ca/errors/proforma_already_accepted) | `invalid_request_error` | 422 | El client ja va acceptar la proforma, i l'acceptació es registra una sola vegada. | | [`proforma_already_rejected`](/ca/errors/proforma_already_rejected) | `invalid_request_error` | 422 | La proforma ja està marcada com a rebutjada. | | [`proforma_cannot_be_accepted`](/ca/errors/proforma_cannot_be_accepted) | `invalid_request_error` | 422 | L'acceptació no escau des de l'estat actual: una proforma facturada, cancel·lada o expirada ja no l'admet. | | [`proforma_cannot_be_rejected`](/ca/errors/proforma_cannot_be_rejected) | `invalid_request_error` | 422 | El rebuig no escau des de l'estat actual: un cop facturada, cancel·lada o expirada, la proforma està tancada. | | [`proforma_cannot_be_sent`](/ca/errors/proforma_cannot_be_sent) | `invalid_request_error` | 422 | L'enviament per correu no s'aplica a una proforma en estat terminal: no hi ha oferta viva a lliurar. | | [`proforma_invalid_status_transition`](/ca/errors/proforma_invalid_status_transition) | `invalid_request_error` | 422 | L'estat destí no és assolible des de l'actual: un esborrany s'accepta, es cancel·la o expira; una proforma acceptada es factura, es rebutja o expira; facturada, cancel·lada i expirada són terminals. | | [`proforma_not_convertible_in_current_state`](/ca/errors/proforma_not_convertible_in_current_state) | `invalid_request_error` | 422 | Convertir en factura exigeix que el client hagi acceptat la proforma; des de qualsevol altre estat no hi ha acord a facturar. | | [`proforma_not_deletable_in_current_state`](/ca/errors/proforma_not_deletable_in_current_state) | `invalid_request_error` | 422 | Només s'esborra una proforma en esborrany. Un cop acceptada, rebutjada o facturada forma part del rastre comercial. | | [`proforma_not_draft`](/ca/errors/proforma_not_draft) | `invalid_request_error` | 422 | L'operació només té sentit mentre la proforma és un esborrany, i aquesta ja ha avançat. | | [`proforma_not_editable_in_current_state`](/ca/errors/proforma_not_editable_in_current_state) | `invalid_request_error` | 422 | Només una proforma en esborrany admet edició. Un cop acceptada, rebutjada, expirada, facturada o cancel·lada, el seu contingut queda fixat. | | [`proforma_not_found`](/ca/errors/proforma_not_found) | `not_found_error` | 404 | L'identificador no resol a cap proforma de l'empresa autenticada. | | [`proforma_requires_at_least_one_line`](/ca/errors/proforma_requires_at_least_one_line) | `invalid_request_error` | 422 | La proforma no porta línies, així que no hi ha import a posar davant del client. | | [`public_link_expires_at_exceeds_max_days`](/ca/errors/public_link_expires_at_exceeds_max_days) | `invalid_request_error` | 422 | La caducitat demanada per a l'enllaç públic supera la finestra màxima que permet el teu pla per a documents compartits. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Factures de compra (/ca/errors/index-purchase-invoices) Codis d'error que emet Factures de compra. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`attachment_invalid_filename`](/ca/errors/attachment_invalid_filename) | `invalid_request_error` | 422 | El nom del fitxer no és utilitzable: és buit, porta components de ruta, o supera els 200 caràcters. | | [`attachment_mime_not_allowed`](/ca/errors/attachment_mime_not_allowed) | `invalid_request_error` | 422 | El tipus de fitxer queda fora del conjunt admès: PDF, PNG, JPEG, XML i HTML. | | [`attachment_missing`](/ca/errors/attachment_missing) | `not_found_error` | 404 | La factura de compra existeix però no té fitxer adjunt, així que no hi ha res a descarregar. | | [`attachment_too_large`](/ca/errors/attachment_too_large) | `invalid_request_error` | 422 | El fitxer supera la mida màxima permesa per a un adjunt de document. | | [`cannot_attach_to_cancelled_purchase_invoice`](/ca/errors/cannot_attach_to_cancelled_purchase_invoice) | `invalid_request_error` | 422 | La factura està cancel·lada, i adjuntar documents a un registre cancel·lat alteraria documentació ja tancada. | | [`invalid_purchase_invoice_id`](/ca/errors/invalid_purchase_invoice_id) | `invalid_request_error` | 400 | La referència de factura de compra rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. | | [`invalid_purchase_invoice_number`](/ca/errors/invalid_purchase_invoice_number) | `invalid_request_error` | 422 | El número de factura és buit o no encaixa amb el format admès. En una factura de compra el número és el que va imprimir el proveïdor, no un que generi Factuarea. | | [`invalid_purchase_invoice_uuid`](/ca/errors/invalid_purchase_invoice_uuid) | `invalid_request_error` | 400 | L'identificador de factura de compra de la ruta o del payload no és un UUID vàlid. | | [`operation_regime_invalid`](/ca/errors/operation_regime_invalid) | `invalid_request_error` | 422 | El règim d'operació queda fora del catàleg `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. | | [`purchase_invoice_already_exists`](/ca/errors/purchase_invoice_already_exists) | `conflict_error` | 409 | Aquest proveïdor ja té registrada una factura de compra amb el mateix número. El parell proveïdor + número identifica el document sense ambigüitat i evita comptabilitzar dues vegades la mateixa despesa. | | [`purchase_invoice_not_deletable_in_current_state`](/ca/errors/purchase_invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Només s'esborren les factures de compra en esborrany o cancel·lades. Una de pendent o pagada forma part del llibre de despeses. | | [`purchase_invoice_not_draft`](/ca/errors/purchase_invoice_not_draft) | `invalid_request_error` | 422 | L'operació només s'aplica mentre la factura de compra és un esborrany, i aquesta ja està registrada. | | [`purchase_invoice_not_editable_in_current_state`](/ca/errors/purchase_invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Només s'edita una factura de compra en esborrany. Un cop registrada com a pendent, pagada o cancel·lada, el seu contingut dona suport a un apunt comptable. | | [`purchase_invoice_not_found`](/ca/errors/purchase_invoice_not_found) | `not_found_error` | 404 | L'identificador no resol a cap factura de compra de l'empresa autenticada. | | [`purchase_invoice_requires_at_least_one_line`](/ca/errors/purchase_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura de compra no porta línies, així que no hi ha despesa ni IVA suportat a registrar. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Pressupostos (/ca/errors/index-quotes) Codis d'error que emet Pressupostos. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [`quote_already_accepted`](/ca/errors/quote_already_accepted) | `invalid_request_error` | 422 | El pressupost ja estava aprovat, i l'aprovació es registra una sola vegada. | | [`quote_already_rejected`](/ca/errors/quote_already_rejected) | `invalid_request_error` | 422 | El pressupost ja està marcat com a rebutjat. | | [`quote_expired`](/ca/errors/quote_expired) | `invalid_request_error` | 422 | El pressupost va passar la seva data de validesa, així que les condicions ofertes ja no vinculen i no es pot aprovar ni convertir tal com està. | | [`quote_not_found`](/ca/errors/quote_not_found) | `not_found_error` | 404 | L'identificador no resol a cap pressupost de l'empresa autenticada. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Límit de peticions (/ca/errors/index-rate-limit) Codis d'error que emet Límit de peticions. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ------------------ | ---- | ------------------------------------------------------------------------------------ | | [`monthly_quota_exceeded`](/ca/errors/monthly_quota_exceeded) | `rate_limit_error` | 429 | L'empresa va esgotar la quota mensual de crides que inclou el seu pla. | | [`rate_limit_exceeded`](/ca/errors/rate_limit_exceeded) | `rate_limit_error` | 429 | La clau va enviar més peticions de les que permet el seu ritme a la finestra actual. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Factures recurrents (/ca/errors/index-recurring-invoices) Codis d'error que emet Factures recurrents. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_frequency_interval`](/ca/errors/invalid_frequency_interval) | `invalid_request_error` | 422 | L'interval és menor que 1, així que la recurrència mai avançaria a una execució següent. | | [`invalid_frequency_type`](/ca/errors/invalid_frequency_type) | `invalid_request_error` | 422 | La freqüència queda fora del catàleg `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. | | [`invalid_holiday_handling`](/ca/errors/invalid_holiday_handling) | `invalid_request_error` | 422 | La política de festius queda fora del catàleg `skip`, `before`, `after`, `same`. | | [`invalid_recurring_invoice_id`](/ca/errors/invalid_recurring_invoice_id) | `invalid_request_error` | 400 | La referència de recurrència rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. | | [`invalid_recurring_invoice_uuid`](/ca/errors/invalid_recurring_invoice_uuid) | `invalid_request_error` | 400 | L'identificador de recurrència de la ruta o del payload no és un UUID vàlid. | | [`recurring_already_active`](/ca/errors/recurring_already_active) | `invalid_request_error` | 422 | La recurrència ja està en marxa, així que no hi ha res a activar. Codi antic conservat per compatibilitat: els endpoints actuals reporten això com a `recurring_invoice_already_active`. | | [`recurring_invoice_already_active`](/ca/errors/recurring_invoice_already_active) | `invalid_request_error` | 422 | La recurrència ja està en marxa. | | [`recurring_invoice_already_cancelled`](/ca/errors/recurring_invoice_already_cancelled) | `invalid_request_error` | 422 | La recurrència ja estava cancel·lada, i la cancel·lació és terminal. | | [`recurring_invoice_already_paused`](/ca/errors/recurring_invoice_already_paused) | `invalid_request_error` | 422 | La recurrència ja està pausada, així que pausar-la un altre cop no canvia res. | | [`recurring_invoice_cancelled_cannot_resume`](/ca/errors/recurring_invoice_cancelled_cannot_resume) | `invalid_request_error` | 422 | Una recurrència cancel·lada no es reprèn: la cancel·lació la tanca definitivament, a diferència de la pausa. | | [`recurring_invoice_cannot_run`](/ca/errors/recurring_invoice_cannot_run) | `invalid_request_error` | 422 | La recurrència no pot generar una factura ara mateix: no està en marxa, el seu cicle s'ha acabat, o li falten dades que la factura necessita. `error.message` indica el motiu concret. | | [`recurring_invoice_has_generated_invoices`](/ca/errors/recurring_invoice_has_generated_invoices) | `invalid_request_error` | 422 | La recurrència ja va generar factures, i aquestes factures en depenen per a la seva traçabilitat. | | [`recurring_invoice_not_found`](/ca/errors/recurring_invoice_not_found) | `not_found_error` | 404 | L'identificador no resol a cap recurrència de l'empresa autenticada. | | [`recurring_invoice_requires_at_least_one_line`](/ca/errors/recurring_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La recurrència no porta línies, així que cada factura generada sortiria buida. | | [`recurring_not_active`](/ca/errors/recurring_not_active) | `invalid_request_error` | 422 | L'operació necessita una recurrència en marxa i aquesta està pausada, completada o cancel·lada. Codi antic conservat per compatibilitat amb integracions velles. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Request (/ca/errors/index-request) Codis d'error que emet Request. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`business_rule_violation`](/ca/errors/business_rule_violation) | `invalid_request_error` | 422 | Una invariant del domini va rebutjar l'operació. Aquest codi indica la família; `error.subcode` anomena la regla concreta i `error.message` l'explica. | | [`conflicting_pagination_params`](/ca/errors/conflicting_pagination_params) | `invalid_request_error` | 422 | `starting_after` i `ending_before` van viatjar a la mateixa petició. Recorren la col·lecció en sentits oposats, així que només se'n pot aplicar un. | | [`external_id_already_exists`](/ca/errors/external_id_already_exists) | `conflict_error` | 409 | L'`external_id` amb què concilies contra el teu sistema ja està assignat a un altre objecte del mateix tipus en aquesta empresa. | | [`invalid_param_format`](/ca/errors/invalid_param_format) | `invalid_request_error` | 422 | Un form request antic va rebutjar la forma d'un valor. Els endpoints migrats reporten el mateix com a `parameter_invalid_format` o `parameter_invalid_integer`. | | [`invalid_param_value`](/ca/errors/invalid_param_value) | `invalid_request_error` | 422 | Un form request antic va rebutjar el valor d'un camp. Els endpoints migrats reporten el mateix com a `parameter_invalid_enum` o `parameter_invalid_range`. | | [`invalid_status_transition`](/ca/errors/invalid_status_transition) | `invalid_request_error` | 422 | L'estat sol·licitat no és assolible des de l'estat en què es troba ara mateix el document. | | [`length_required`](/ca/errors/length_required) | `invalid_request_error` | 411 | Va arribar una petició amb body en codificació chunked, sense declarar-ne la mida. L'API necessita conèixer la longitud per avançat per rebutjar payloads excessius abans de carregar-los a memòria. | | [`metadata_too_many_keys`](/ca/errors/metadata_too_many_keys) | `invalid_request_error` | 422 | L'objecte `metadata` supera el límit de 50 claus per recurs. | | [`metadata_value_too_long`](/ca/errors/metadata_value_too_long) | `invalid_request_error` | 422 | Un valor de `metadata` supera els 500 caràcters un cop serialitzat a text. | | [`method_not_allowed`](/ca/errors/method_not_allowed) | `invalid_request_error` | 405 | La ruta existeix però no accepta el verb HTTP utilitzat. | | [`missing_required_param`](/ca/errors/missing_required_param) | `invalid_request_error` | 422 | Un form request antic va detectar que faltava un camp obligatori. Els endpoints ja migrats als parsers canònics reporten el mateix com a `parameter_missing`. | | [`parameter_invalid`](/ca/errors/parameter_invalid) | `invalid_request_error` | 422 | Un value object construït a partir del payload va rebutjar el valor rebut. `error.subcode` diu quin: codi d'impost, codi de país, tipus impositiu, etc. | | [`parameter_invalid_boolean`](/ca/errors/parameter_invalid_boolean) | `invalid_request_error` | 400 | Un paràmetre que ha de ser booleà va rebre un valor fora de les representacions acceptades (`true`/`false`, `1`/`0`). | | [`parameter_invalid_cursor`](/ca/errors/parameter_invalid_cursor) | `invalid_request_error` | 400 | El cursor `starting_after` o `ending_before` no és un UUID vàlid, així que no pot apuntar a cap fila de la col·lecció. | | [`parameter_invalid_empty`](/ca/errors/parameter_invalid_empty) | `invalid_request_error` | 400 | Un paràmetre va arribar amb el valor buit: un filtre `in` sense elements, una comparació sense res després de l'operador, o un filtre d'igualtat amb la cadena buida. | | [`parameter_invalid_enum`](/ca/errors/parameter_invalid_enum) | `invalid_request_error` | 400 | El valor queda fora del conjunt tancat que accepta el paràmetre. En els llistats cobreix a més un operador de filtre diferent de `eq`, `gte`, `lte`, `gt`, `lt`, `in` o `contains`. | | [`parameter_invalid_format`](/ca/errors/parameter_invalid_format) | `invalid_request_error` | 400 | El valor té el tipus correcte però no la forma que exigeix el paràmetre: una data, un patró d'identificador o una capçalera com `Factuarea-Version`. | | [`parameter_invalid_integer`](/ca/errors/parameter_invalid_integer) | `invalid_request_error` | 400 | Un paràmetre que ha de ser un nombre enter va rebre alguna cosa que no es pot interpretar com a tal, per exemple `limit=abc`. | | [`parameter_invalid_iso8601`](/ca/errors/parameter_invalid_iso8601) | `invalid_request_error` | 400 | Un filtre de rang (`gte`, `lte`, `gt`, `lt`) va rebre un valor que no és numèric ni una data ISO 8601. | | [`parameter_invalid_range`](/ca/errors/parameter_invalid_range) | `invalid_request_error` | 400 | Un paràmetre numèric va quedar fora dels seus límits. El cas habitual és `limit`, que ha d'estar entre 1 i 100. | | [`parameter_invalid_string`](/ca/errors/parameter_invalid_string) | `invalid_request_error` | 400 | Un paràmetre que ha de ser text va rebre un array, un objecte o un valor que no es pot llegir com a cadena. | | [`parameter_invalid_url`](/ca/errors/parameter_invalid_url) | `invalid_request_error` | 400 | Un camp que ha de contenir una URL absoluta va rebre un valor que no ho és, normalment perquè li falta l'esquema o l'amfitrió. | | [`parameter_invalid_uuid`](/ca/errors/parameter_invalid_uuid) | `invalid_request_error` | 400 | Un camp d'identificador va rebre un valor que no és un UUID vàlid. Tot `id` de recurs a v1 és un UUID. | | [`parameter_invalid_value`](/ca/errors/parameter_invalid_value) | `invalid_request_error` | 422 | El valor és sintàcticament correcte però no admissible per a aquest recurs: fora del catàleg canònic del camp, o incoherent amb la resta del payload. | | [`parameter_missing`](/ca/errors/parameter_missing) | `invalid_request_error` | 400 | L'endpoint exigeix un paràmetre que la petició no portava. `error.param` diu quin. | | [`parameter_unknown`](/ca/errors/parameter_unknown) | `invalid_request_error` | 400 | La petició porta un paràmetre que l'endpoint no accepta: un filtre fora de la seva allowlist, un camp de `sort` no ordenable, o el `page` de paginació per offset — v1 pagina per cursor. | | [`payload_too_large`](/ca/errors/payload_too_large) | `invalid_request_error` | 413 | El body de la petició supera la mida admesa: 1 MB amb caràcter general, 6 MB als endpoints que accepten fitxers. | | [`profile_not_found`](/ca/errors/profile_not_found) | `not_found_error` | 404 | La capçalera `X-Active-Profile` anomena una empresa que no existeix o que no pertany a l'arbre de gestoria de la clau autenticada. Tots dos casos responen igual perquè l'API mai reveli empreses d'altres tenants. | | [`resource_already_exists`](/ca/errors/resource_already_exists) | `conflict_error` | 409 | Crear l'objecte duplicaria un que ja existeix sota una clau única — NIF, SKU, external id. `error.details.existing_resource_id` apunta a l'objecte que ja ocupa aquest valor. | | [`resource_conflict`](/ca/errors/resource_conflict) | `conflict_error` | 409 | L'operació va xocar amb l'estat actual del recurs i no s'aplica cap codi de conflicte més específic. | | [`resource_immutable`](/ca/errors/resource_immutable) | `invalid_request_error` | 422 | L'objecte està tancat a canvis per a aquesta operació: el seu estat o el seu registre comptable impedeixen modificar-lo. | | [`resource_locked`](/ca/errors/resource_locked) | `conflict_error` | 409 | Una altra operació reté el recurs fins que acaba: les escriptures concurrents sobre el mateix objecte se serialitzen en lloc d'entrellaçar-se. | | [`resource_not_deletable`](/ca/errors/resource_not_deletable) | `invalid_request_error` | 422 | L'objecte existeix, però el seu estat o els seus dependents bloquegen l'esborrat. En els esborrats massius aquest és el codi per fila de cada entrada que no es va poder eliminar. | | [`resource_not_found`](/ca/errors/resource_not_found) | `not_found_error` | 404 | L'identificador no resol a res visible per a l'empresa autenticada. Els objectes d'una altra empresa responen exactament igual, a propòsit. | | [`route_not_found`](/ca/errors/route_not_found) | `not_found_error` | 404 | La ruta no correspon a cap endpoint de v1. Sol ser una errada, un prefix `/v1` absent o una ruta d'una altra àrea de l'API. | | [`unknown_filter`](/ca/errors/unknown_filter) | `invalid_request_error` | 422 | Un llistat va rebre un filtre que no coneix. Els parsers canònics de v1 reporten això com a `parameter_unknown`; aquest codi sobreviu per als endpoints encara sense migrar. | | [`unsupported_api_version`](/ca/errors/unsupported_api_version) | `invalid_request_error` | 400 | La capçalera `Factuarea-Version` està ben formada però anomena una versió fora del conjunt suportat. | | [`unsupported_media_type`](/ca/errors/unsupported_media_type) | `invalid_request_error` | 415 | Una petició amb body va declarar un `Content-Type` diferent de `application/json`. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Series (/ca/errors/index-series) Codis d'error que emet Series. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`cannot_archive_last_default_series`](/ca/errors/cannot_archive_last_default_series) | `invalid_request_error` | 422 | La sèrie és l'única activa del seu tipus de document. Arxivar-la deixaria l'empresa sense numeració disponible i congelaria aquest tipus de document. | | [`document_type_required_for_ambiguous_code`](/ca/errors/document_type_required_for_ambiguous_code) | `invalid_request_error` | 422 | Aquest codi de sèrie existeix per a més d'un tipus de document, així que per si sol no identifica una única sèrie. | | [`invalid_series_code`](/ca/errors/invalid_series_code) | `invalid_request_error` | 422 | El codi de la sèrie és buit, massa llarg, o porta caràcters que no corresponen a un prefix fiscal. | | [`invalid_series_name`](/ca/errors/invalid_series_name) | `invalid_request_error` | 422 | El nom de la sèrie és buit o supera la longitud permesa. | | [`invalid_series_number`](/ca/errors/invalid_series_number) | `invalid_request_error` | 422 | El número inicial no és vàlid: no és un enter positiu, o queda a l'últim número ja emès o per sota, cosa que reemetria números ja consumits. | | [`invalid_series_uuid`](/ca/errors/invalid_series_uuid) | `invalid_request_error` | 400 | L'identificador de sèrie de la ruta o del payload no és un UUID vàlid. | | [`invalid_series_year`](/ca/errors/invalid_series_year) | `invalid_request_error` | 422 | L'exercici no és un any de quatre xifres vàlid per a una sèrie de numeració. | | [`monthly_requires_month_segmented_format`](/ca/errors/monthly_requires_month_segmented_format) | `invalid_request_error` | 422 | El comptador es reinicia cada mes però la màscara de numeració no segrega per mes, així que dos mesos arrencarien al mateix correlatiu i produirien números duplicats dins de l'any. | | [`series_already_archived`](/ca/errors/series_already_archived) | `invalid_request_error` | 422 | La sèrie ja estava arxivada, i l'arxivat no es repeteix: una segona crida indica que el client ha perdut l'estat real. | | [`series_code_immutable_with_documents`](/ca/errors/series_code_immutable_with_documents) | `invalid_request_error` | 422 | Canviar el prefix d'una sèrie que ja va emetre documents reescriuria retroactivament el seu identificador fiscal, mentre els clients i l'AEAT tenen el número original. | | [`series_has_documents`](/ca/errors/series_has_documents) | `invalid_request_error` | 422 | La sèrie ja va numerar documents, així que no es pot eliminar: la seqüència correlativa ha de seguir sent auditable. | | [`series_immutable`](/ca/errors/series_immutable) | `invalid_request_error` | 405 | Les sèries no són editables ni eliminables via API: la continuïtat legal de la numeració exigeix que el seu prefix, el seu any i el seu comptador es quedin com estan. | | [`series_initial_number_creates_gap`](/ca/errors/series_initial_number_creates_gap) | `invalid_request_error` | 422 | El número inicial salta més enllà del següent correlatiu natural havent-hi documents de l'any en curs, i aquest buit a la seqüència no és admissible per a l'AEAT. | | [`series_locked_by_verifactu`](/ca/errors/series_locked_by_verifactu) | `invalid_request_error` | 422 | Com a mínim una factura de la sèrie té un registre de facturació acceptat per l'AEAT, cosa que congela el prefix, l'any i la base de numeració de la sèrie. | | [`series_not_found`](/ca/errors/series_not_found) | `not_found_error` | 404 | L'identificador no resol a cap sèrie de numeració de l'empresa autenticada. | | [`series_type_invalid`](/ca/errors/series_type_invalid) | `invalid_request_error` | 422 | El tipus de document de la sèrie queda fora del catàleg `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`series_year_locked`](/ca/errors/series_year_locked) | `invalid_request_error` | 422 | La sèrie ja va emetre documents en el seu any vigent. Moure l'any deixaria aquests documents apuntant a un exercici buit mentre la seva base imposable és en un altre. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Servidor (/ca/errors/index-server) Codis d'error que emet Servidor. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------- | --------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`dependency_unavailable`](/ca/errors/dependency_unavailable) | `service_unavailable_error` | 503 | Un servei extern del qual depèn l'operació no va respondre a temps. | | [`face_transmission_failed`](/ca/errors/face_transmission_failed) | `api_error` | 502 | La plataforma FACe —el punt d'entrada de les administracions públiques— era inaccessible o va respondre amb una fallada. El problema és aigües amunt, no a la teva petició. | | [`facturae_signing_failed`](/ca/errors/facturae_signing_failed) | `api_error` | 500 | No es va poder produir la signatura XAdES del fitxer Facturae, normalment perquè el certificat de signatura no és utilitzable en aquell moment. | | [`internal_error`](/ca/errors/internal_error) | `api_error` | 500 | Alguna cosa s'ha trencat al nostre costat en processar la petició. La condició no la provoca el teu payload. | | [`maintenance`](/ca/errors/maintenance) | `service_unavailable_error` | 503 | La plataforma és en finestra de manteniment i les escriptures es retenen a propòsit. | | [`pdf_generation_failed`](/ca/errors/pdf_generation_failed) | `service_unavailable_error` | 503 | El servei de renderitzat no va poder produir el PDF. El document i les seves dades són intactes: el que ha fallat és el fitxer. | | [`register_sealing_failed`](/ca/errors/register_sealing_failed) | `api_error` | 500 | El segellat criptogràfic del registre no es va completar, així que el tancament va quedar sense signar en lloc de segellat amb una signatura trencada. | | [`send_failed`](/ca/errors/send_failed) | `api_error` | 500 | El document no es va lliurar per correu: el proveïdor de correu va rebutjar el missatge o era inaccessible. | | [`service_unavailable`](/ca/errors/service_unavailable) | `service_unavailable_error` | 503 | El servei, o una dependència que necessita, no pot respondre temporalment. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Proveïdors (/ca/errors/index-suppliers) Codis d'error que emet Proveïdors. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [`supplier_has_documents`](/ca/errors/supplier_has_documents) | `invalid_request_error` | 422 | El proveïdor està referenciat per factures de compra registrades, i esborrar-lo deixaria aquestes despeses sense la part que les va emetre. | | [`supplier_not_found`](/ca/errors/supplier_not_found) | `not_found_error` | 404 | L'identificador no resol a cap proveïdor de l'empresa autenticada. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Informes fiscals (/ca/errors/index-tax-reports) Codis d'error que emet Informes fiscals. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [`insufficient_data_for_report`](/ca/errors/insufficient_data_for_report) | `invalid_request_error` | 422 | El període no té dades a declarar, o a una factura del període li falta un camp obligatori per a aquest model, típicament el NIF del client. | | [`invalid_period`](/ca/errors/invalid_period) | `invalid_request_error` | 422 | El període no identifica una declaració: l'any queda fora del rang admès, o falta el trimestre o és fora del rang 1 a 4 en un model trimestral. | | [`report_format_invalid`](/ca/errors/report_format_invalid) | `invalid_request_error` | 422 | El format queda fora del catàleg `txt_aeat`, `pdf`, `excel`. | | [`tax_report_not_found`](/ca/errors/tax_report_not_found) | `not_found_error` | 404 | L'identificador no resol a cap declaració de l'empresa autenticada. | | [`tax_report_type_invalid`](/ca/errors/tax_report_type_invalid) | `invalid_request_error` | 422 | El tipus de declaració queda fora del catàleg `modelo_303`, `modelo_347`, `modelo_130`. | | [`unsupported_format`](/ca/errors/unsupported_format) | `invalid_request_error` | 422 | El format demanat no està disponible per a aquest model: no tota declaració produeix totes les sortides. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error d'Impostos (/ca/errors/index-taxes) Codis d'error que emet Impostos. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`custom_tax_creation_disabled`](/ca/errors/custom_tax_creation_disabled) | `authorization_error` | 403 | La creació d'impostos personalitzats està deshabilitada per a aquesta empresa. | | [`duplicate_tax_default_for_document_type`](/ca/errors/duplicate_tax_default_for_document_type) | `invalid_request_error` | 422 | Ja hi ha un altre impost del mateix tipus marcat com a default per a aquest tipus de document, i el parell (tipus d'impost, tipus de document) admet un únic default. | | [`indirect_tax_regime_invalid`](/ca/errors/indirect_tax_regime_invalid) | `invalid_request_error` | 422 | El règim indirecte queda fora del catàleg `iva`, `igic`, `ipsi`. | | [`invalid_aeat_code`](/ca/errors/invalid_aeat_code) | `invalid_request_error` | 422 | El codi d'operació AEAT queda fora del catàleg tancat `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` que fan servir VeriFactu i el SII. | | [`invalid_country_aeat_zone`](/ca/errors/invalid_country_aeat_zone) | `invalid_request_error` | 422 | La zona territorial AEAT queda fora del catàleg `peninsula`, `canarias`, `ceuta`, `melilla`. | | [`invalid_country_code`](/ca/errors/invalid_country_code) | `invalid_request_error` | 422 | El codi de país no té exactament dos caràcters, així que no és un codi ISO 3166-1 alfa-2 vàlid. | | [`invalid_customer_visible_label`](/ca/errors/invalid_customer_visible_label) | `invalid_request_error` | 422 | L'etiqueta que es mostra al client al document supera la longitud permesa. | | [`invalid_description`](/ca/errors/invalid_description) | `invalid_request_error` | 422 | La descripció supera la longitud màxima permesa per al camp. | | [`invalid_document_type`](/ca/errors/invalid_document_type) | `invalid_request_error` | 422 | El tipus de document queda fora del catàleg: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`invalid_rate_for_tax_regime`](/ca/errors/invalid_rate_for_tax_regime) | `invalid_request_error` | 422 | El tipus no pertany a la graella legal del seu règim: l'IGIC admet 0, 3, 5, 7, 9,5, 15 i 20 %; l'IPSI admet 0, 0,5, 1, 2, 4, 8 i 10 %. | | [`invalid_tax_code`](/ca/errors/invalid_tax_code) | `invalid_request_error` | 422 | El codi de l'impost és buit o supera els 50 caràcters. | | [`invalid_tax_name`](/ca/errors/invalid_tax_name) | `invalid_request_error` | 422 | El nom de l'impost és buit o supera els 255 caràcters. | | [`invalid_tax_rate`](/ca/errors/invalid_tax_rate) | `invalid_request_error` | 422 | El tipus impositiu queda fora del rang permès per a la seva classe: IVA 0-27 %, retenció 0-47 %, recàrrec d'equivalència 0-10 %, altres 0-100 %. | | [`invalid_tax_type_filter`](/ca/errors/invalid_tax_type_filter) | `invalid_request_error` | 422 | El filtre `type` del llistat per tipus porta un valor fora de l'enum `vat`, `retention`, `surcharge`, `other`. | | [`invalid_validity_window`](/ca/errors/invalid_validity_window) | `invalid_request_error` | 422 | La finestra de vigència està invertida: `valid_until` és anterior a `valid_from`. | | [`system_tax_default_modification_forbidden`](/ca/errors/system_tax_default_modification_forbidden) | `authorization_error` | 403 | Els defaults dels impostos del catàleg compartit no es fixen sobre l'impost: el catàleg és global i la preferència és de la teva empresa. | | [`system_tax_immutable`](/ca/errors/system_tax_immutable) | `invalid_request_error` | 422 | L'impost pertany al catàleg canònic AEAT que porta el producte. El seu tipus, el seu codi i el seu nom són fixos perquè totes les empreses comparteixin la mateixa referència fiscal. | | [`system_tax_immutable_field`](/ca/errors/system_tax_immutable_field) | `invalid_request_error` | 422 | L'actualització toca un camp congelat en un impost del sistema; `error.param` diu quin. | | [`system_tax_undeletable`](/ca/errors/system_tax_undeletable) | `invalid_request_error` | 422 | Els impostos del sistema formen part del catàleg fiscal compartit i no s'eliminen: esborrar-los trencaria els documents que els referencien. | | [`tax_applies_to_invalid`](/ca/errors/tax_applies_to_invalid) | `invalid_request_error` | 422 | L'àmbit de l'impost queda fora del catàleg `sale`, `purchase`, `both`. | | [`tax_code_already_exists`](/ca/errors/tax_code_already_exists) | `conflict_error` | 409 | Un altre impost del catàleg ja fa servir aquest codi, i el codi identifica l'impost sense ambigüitat. | | [`tax_id_required`](/ca/errors/tax_id_required) | `invalid_request_error` | 422 | L'operació necessita el número d'identificació fiscal (NIF, CIF o NIE) de la part implicada i el registre no en té. | | [`tax_in_use`](/ca/errors/tax_in_use) | `invalid_request_error` | 422 | L'impost està referenciat per documents, productes o proveïdors. Eliminar-lo deixaria documents històrics sense la seva referència fiscal. | | [`tax_inactive_cannot_be_default`](/ca/errors/tax_inactive_cannot_be_default) | `invalid_request_error` | 422 | Un impost desactivat no pot quedar com a default, ni global ni per tipus de document: seria un default ocult que cap formulari pot triar. | | [`tax_not_found`](/ca/errors/tax_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap impost del catàleg accessible per a aquesta empresa. | | [`tax_type_invalid`](/ca/errors/tax_type_invalid) | `invalid_request_error` | 422 | El tipus d'impost queda fora del catàleg `vat`, `retention`, `surcharge`, `other`. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de VeriFactu (/ca/errors/index-verifactu) Codis d'error que emet VeriFactu. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alta_record_not_found`](/ca/errors/alta_record_not_found) | `not_found_error` | 404 | La factura no té registre d'alta, així que l'operació que en depèn no té sobre què treballar. | | [`anulacion_record_already_exists`](/ca/errors/anulacion_record_already_exists) | `conflict_error` | 409 | La factura ja té un registre d'anul·lació a la cadena, i l'anul·lació es declara una sola vegada. | | [`certificate_expired`](/ca/errors/certificate_expired) | `invalid_request_error` | 422 | El certificat està fora de la seva finestra de validesa: ha caducat, o encara no és vàlid. | | [`certificate_nif_mismatch`](/ca/errors/certificate_nif_mismatch) | `invalid_request_error` | 422 | El NIF del titular del certificat no coincideix amb el de l'empresa. Els registres AEAT es signen en nom de l'empresa, així que tots dos han de ser el mateix. | | [`certificate_not_found`](/ca/errors/certificate_not_found) | `not_found_error` | 404 | L'empresa no té cap certificat FNMT que correspongui a l'identificador, o no en té cap de pujat. | | [`certificate_too_large`](/ca/errors/certificate_too_large) | `invalid_request_error` | 422 | El fitxer supera el límit de 100 KB, quan un certificat FNMT real pesa uns pocs kilobytes. | | [`clock_drift_exceeded`](/ca/errors/clock_drift_exceeded) | `invalid_request_error` | 422 | El rellotge del servidor es va desviar de l'NTP per sobre del marge permès. La marca de temps de generació entra a l'empremta AEAT, així que un rellotge desincronitzat produiria registres que l'AEAT rebutja. | | [`declaracion_already_exists`](/ca/errors/declaracion_already_exists) | `conflict_error` | 409 | L'empresa ja té presentada la declaració responsable del SIF d'aquest període. | | [`declaracion_not_found`](/ca/errors/declaracion_not_found) | `not_found_error` | 404 | L'empresa no té presentada la declaració responsable del SIF del període sol·licitat. | | [`event_already_processed`](/ca/errors/event_already_processed) | `invalid_request_error` | 422 | Aquest esdeveniment del SIF ja consta a la cadena d'esdeveniments, i cada esdeveniment es processa exactament una vegada. | | [`invalid_certificate_format`](/ca/errors/invalid_certificate_format) | `invalid_request_error` | 422 | El fitxer no és un contenidor PKCS#12: els seus primers bytes no corresponen a l'estructura ASN.1 que exigeix el format, digui el que digui l'extensió. | | [`invalid_certificate_password`](/ca/errors/invalid_certificate_password) | `invalid_request_error` | 422 | La contrasenya no obre el fitxer del certificat. | | [`max_retries_exceeded`](/ca/errors/max_retries_exceeded) | `invalid_request_error` | 422 | El registre va esgotar el pressupost de reintents tècnics de reenviament de l'XML emmagatzemat. Reintentar el mateix contingut tornaria a fallar igual. | | [`mode_switch_blocked_until_year_end`](/ca/errors/mode_switch_blocked_until_year_end) | `invalid_request_error` | 422 | El mode VeriFactu es va activar en aquest exercici i ja es va emetre com a mínim un registre de facturació. Fer marxa enrere degradaria la integritat d'una cadena ja declarada a l'AEAT. | | [`record_already_accepted`](/ca/errors/record_already_accepted) | `invalid_request_error` | 422 | L'AEAT ja va acceptar el registre. L'acceptació és terminal i el seu contingut queda congelat com a part de la cadena d'empremtes. | | [`record_immutable`](/ca/errors/record_immutable) | `invalid_request_error` | 422 | El registre pertany a un ledger de només-addició: un cop escrit, el seu contingut fiscal queda tancat a modificacions i a esborrat. | | [`record_not_rejected`](/ca/errors/record_not_rejected) | `invalid_request_error` | 422 | L'esmena només s'aplica a registres que l'AEAT va rebutjar per dades. Aquest registre està en un altre estat — una fallada tècnica, per exemple, la cobreix el reintent automàtic. | | [`record_not_subsanable`](/ca/errors/record_not_subsanable) | `invalid_request_error` | 422 | El registre no es pot esmenar: no és un registre d'alta, o no té factura d'origen des de la qual regenerar-ne el contingut. | | [`requires_annulment`](/ca/errors/requires_annulment) | `invalid_request_error` | 422 | El contingut regenerat canvia un camp que entra a l'empremta —NIF de l'emissor, sèrie i número, data d'expedició, tipus de factura, quota o import total— i la cadena no es pot reescriure. | | [`sii_excluded`](/ca/errors/sii_excluded) | `invalid_request_error` | 422 | L'empresa està registrada al SII, i els obligats al SII queden exclosos del reglament VeriFactu. | | [`verifactu_already_submitted`](/ca/errors/verifactu_already_submitted) | `invalid_request_error` | 422 | La factura ja té el seu registre d'alta. Existeix exactament una alta per factura, així que una segona trencaria la idempotència de la cadena. | | [`verifactu_mode_invalid`](/ca/errors/verifactu_mode_invalid) | `invalid_request_error` | 422 | El mode queda fora del catàleg `verifactu` / `no_verifactu`. | | [`verifactu_not_eligible`](/ca/errors/verifactu_not_eligible) | `invalid_request_error` | 422 | La factura no es pot registrar ara mateix a l'AEAT: l'empresa no està en mode VeriFactu, no té certificat actiu, o el certificat està revocat o emès per a un altre NIF. | | [`verifactu_record_not_found`](/ca/errors/verifactu_record_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap registre de facturació de l'empresa autenticada. | | [`verifactu_transmission_failed`](/ca/errors/verifactu_transmission_failed) | `invalid_request_error` | 422 | L'enviament del registre a l'AEAT no es va completar: l'endpoint era inaccessible o va respondre amb una incidència. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # Codis d'error de Webhooks (/ca/errors/index-webhooks) Codis d'error que emet Webhooks. Cada `code` enllaça a la seva pròpia pàgina amb la causa i l'acció a prendre. | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_required`](/ca/errors/addon_required) | `payment_required_error` | 402 | Crear endpoints de webhook pertany a l'add-on Developer API, i l'empresa no el té actiu: el nivell gratuït permet zero endpoints. | | [`api_version_invalid_format`](/ca/errors/api_version_invalid_format) | `invalid_request_error` | 422 | La versió de payload de l'endpoint no és una data `YYYY-MM-DD`. | | [`api_version_unsupported`](/ca/errors/api_version_unsupported) | `invalid_request_error` | 422 | La versió de payload està ben formada però no és entre les que serveix la plataforma. | | [`custom_header_blocklisted`](/ca/errors/custom_header_blocklisted) | `invalid_request_error` | 422 | Una de les capçaleres personalitzades està reservada: la gestiona la capa HTTP (`host`, `content-type`, `content-length`, `user-agent`), l'envia Factuarea com a part del contracte signat (`factuarea-*`), o pertany al proxy (`x-forwarded-*`). | | [`custom_header_value_too_long`](/ca/errors/custom_header_value_too_long) | `invalid_request_error` | 422 | El valor d'una capçalera personalitzada supera els 1024 caràcters. | | [`replay_delivery_not_retryable`](/ca/errors/replay_delivery_not_retryable) | `invalid_request_error` | 422 | Només es reenvien els lliuraments fallits. Un lliurament que va arribar bé, o un encara en curs, no té res a reenviar. | | [`replay_event_expired`](/ca/errors/replay_event_expired) | `invalid_request_error` | 422 | L'esdeveniment que dona suport al lliurament va ser purgat per la política de retenció de 30 dies, així que ja no queda payload a reenviar. | | [`timeout_seconds_out_of_range`](/ca/errors/timeout_seconds_out_of_range) | `invalid_request_error` | 422 | `timeout_seconds` queda fora del rang d'1 a 30 segons. | | [`too_many_custom_headers`](/ca/errors/too_many_custom_headers) | `invalid_request_error` | 422 | L'endpoint declara més de 20 capçaleres personalitzades. | | [`webhook_delivery_not_found`](/ca/errors/webhook_delivery_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap intent de lliurament, o el lliurament queda fora de la finestra de retenció de l'històric. | | [`webhook_endpoint_degraded`](/ca/errors/webhook_endpoint_degraded) | `invalid_request_error` | 422 | L'endpoint està degradat després de fallades repetides de lliurament, així que els pings de prova es rebutgen mentre segueixi en aquest estat. | | [`webhook_endpoint_not_found`](/ca/errors/webhook_endpoint_not_found) | `not_found_error` | 404 | L'identificador no resol a cap endpoint de webhook de l'empresa autenticada. | | [`webhook_secret_recently_rotated`](/ca/errors/webhook_secret_recently_rotated) | `rate_limit_error` | 429 | El secret de signatura es va rotar fa menys de cinc minuts. La finestra de gràcia permet que el teu receptor accepti tots dos secrets durant el canvi; rotar un altre cop dins d'ella invalidaria signatures encara en vol. | ## Relacionat [#relacionat] * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # indirect_tax_regime_invalid (/ca/errors/indirect_tax_regime_invalid) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ---------------------------------- | | `indirect_tax_regime_invalid` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El règim indirecte queda fora del catàleg `iva`, `igic`, `ipsi`. ## Què fer [#què-fer] Envia un dels tres règims, o deixa que es derivi de la zona AEAT en lloc de declarar-lo a mà. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # insufficient_data_for_report (/ca/errors/insufficient_data_for_report) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ------------------------------------------------ | | `insufficient_data_for_report` | `invalid_request_error` | 422 | [Informes fiscals](/ca/errors/index-tax-reports) | ## Causa [#causa] El període no té dades a declarar, o a una factura del període li falta un camp obligatori per a aquest model, típicament el NIF del client. ## Què fer [#què-fer] Llegeix `error.subcode`: completa la dada que falta a les factures que assenyala, o tria un període amb activitat. ## Relacionat [#relacionat] * [Tots els codis d'error d'Informes fiscals](/ca/errors/index-tax-reports) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # insufficient_scope (/ca/errors/insufficient_scope) | Code | Type | HTTP | Categoria | | -------------------- | --------------------- | ---- | ---------------------------------------------- | | `insufficient_scope` | `authorization_error` | 403 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] La clau autentica correctament però no porta l'abast que exigeix aquesta operació. Els abasts es concedeixen en emetre la clau i no s'amplien en temps de crida. ## Què fer [#què-fer] Emet una clau que inclogui l'abast que indica `error.message` —de lectura per a consultes, d'escriptura per a canvis— i fes-la servir per a aquesta crida. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # internal_error (/ca/errors/internal_error) | Code | Type | HTTP | Categoria | | ---------------- | ----------- | ---- | ----------------------------------- | | `internal_error` | `api_error` | 500 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] Alguna cosa s'ha trencat al nostre costat en processar la petició. La condició no la provoca el teu payload. ## Què fer [#què-fer] Reintenta amb retard exponencial, reutilitzant la mateixa `Idempotency-Key` a les escriptures, i comunica el `request_id` si persisteix. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_aeat_code (/ca/errors/invalid_aeat_code) | Code | Type | HTTP | Categoria | | ------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_aeat_code` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El codi d'operació AEAT queda fora del catàleg tancat `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` que fan servir VeriFactu i el SII. ## Què fer [#què-fer] Tria el codi que correspon a la naturalesa fiscal de l'operació: `S1` subjecta i no exempta, `S2` inversió del subjecte passiu, `E1`-`E6` exempcions, `N1`-`N2` no subjecta. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_api_key (/ca/errors/invalid_api_key) | Code | Type | HTTP | Categoria | | ----------------- | ---------------------- | ---- | ----------------------------------------------- | | `invalid_api_key` | `authentication_error` | 401 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] La clau no correspon a cap clau activa. Pot estar mal copiada, truncada, o pertànyer a un altre entorn: les claus de prova i les de producció no són intercanviables. ## Què fer [#què-fer] Torna a copiar la clau del panell i comprova l'entorn: les `fact_test_` només funcionen en mode test i les `fact_live_` només en producció. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Clau API no vàlida. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_certificate_format (/ca/errors/invalid_certificate_format) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------------- | ---- | --------------------------------------- | | `invalid_certificate_format` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El fitxer no és un contenidor PKCS#12: els seus primers bytes no corresponen a l'estructura ASN.1 que exigeix el format, digui el que digui l'extensió. ## Què fer [#què-fer] Puja el fitxer `.p12` o `.pfx` original; un PEM, un CRT o un fitxer reanomenat no s'accepten. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_certificate_password (/ca/errors/invalid_certificate_password) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | --------------------------------------- | | `invalid_certificate_password` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] La contrasenya no obre el fitxer del certificat. ## Què fer [#què-fer] Envia la contrasenya que protegeix el `.p12` tal com es va fixar: els espais i les majúscules compten. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_correction_nature (/ca/errors/invalid_correction_nature) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_correction_nature` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] `correction_nature` només accepta `S` (substitució: la rectificativa porta els imports corregits complets) o `I` (per diferències: només porta el delta). ## Què fer [#què-fer] Envia `S` quan la rectificativa substitueix els imports de l'original, i `I` quan només recull la diferència. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_correction_reason (/ca/errors/invalid_correction_reason) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_correction_reason` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El motiu de rectificació queda fora de la llista fiscal tancada (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), que mapeja als codis AEAT R1 a R4. ## Què fer [#què-fer] Tria el motiu que reflecteixi la causa real: decideix el codi que es declara a l'AEAT, i `concurso` i `incobrable` exigeixen documentació acreditativa. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_country_aeat_zone (/ca/errors/invalid_country_aeat_zone) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_country_aeat_zone` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] La zona territorial AEAT queda fora del catàleg `peninsula`, `canarias`, `ceuta`, `melilla`. ## Què fer [#què-fer] Envia la zona que correspon al territori de l'impost: decideix el règim indirecte (IVA, IGIC o IPSI) i la graella legal de tipus. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_country_code (/ca/errors/invalid_country_code) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_country_code` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El codi de país no té exactament dos caràcters, així que no és un codi ISO 3166-1 alfa-2 vàlid. ## Què fer [#què-fer] Envia el codi de dues lletres del país (`ES`, `FR`, `PT`). ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_customer_visible_label (/ca/errors/invalid_customer_visible_label) | Code | Type | HTTP | Categoria | | -------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_customer_visible_label` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'etiqueta que es mostra al client al document supera la longitud permesa. ## Què fer [#què-fer] Escurça l'etiqueta: està pensada com a rètol breu a la línia del document, no com a descripció. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_description (/ca/errors/invalid_description) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_description` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] La descripció supera la longitud màxima permesa per al camp. ## Què fer [#què-fer] Escurça la descripció; el detall identificatiu va al nom i al codi, no aquí. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_document_type (/ca/errors/invalid_document_type) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_document_type` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El tipus de document queda fora del catàleg: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. ## Què fer [#què-fer] Envia un d'aquests valors al camp que selecciona el tipus de document. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_expiry_date (/ca/errors/invalid_expiry_date) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_expiry_date` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] La data de venciment és anterior a la d'emissió, o la supera en més de 365 dies. ## Què fer [#què-fer] Envia una data de venciment entre la data d'emissió i 365 dies després. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_frequency_interval (/ca/errors/invalid_frequency_interval) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `invalid_frequency_interval` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] L'interval és menor que 1, així que la recurrència mai avançaria a una execució següent. ## Què fer [#què-fer] Envia un interval d'1 o més: multiplica la freqüència, com `monthly` amb interval 2 per a cada dos mesos. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_frequency_type (/ca/errors/invalid_frequency_type) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ---------------------------------------------------------- | | `invalid_frequency_type` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La freqüència queda fora del catàleg `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. ## Què fer [#què-fer] Tria una de les freqüències; fes servir `custom` amb un interval explícit quan cap de les anomenades encaixi. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_holiday_handling (/ca/errors/invalid_holiday_handling) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `invalid_holiday_handling` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La política de festius queda fora del catàleg `skip`, `before`, `after`, `same`. ## Què fer [#què-fer] Tria què ha de passar quan una execució cau en festiu: saltar-la, avançar-la, endarrerir-la, o emetre igualment en aquesta data. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_invoice_id (/ca/errors/invalid_invoice_id) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_id` | `invalid_request_error` | 400 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La referència de factura rebuda no és un identificador vàlid; sol voler dir que s'ha colat un valor intern on l'API espera l'`id` públic. ## Què fer [#què-fer] Envia l'`id` de factura que retorna l'API; els identificadors numèrics interns no formen part del contracte v1. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_invoice_number (/ca/errors/invalid_invoice_number) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_number` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El número de factura no segueix el format canònic `SÈRIE-AAAA-NNN`, més el sufix `-RECn` a les rectificatives. ## Què fer [#què-fer] Envia el número tal com apareix a la factura, en lloc de compondre'l a partir de les seves parts. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_invoice_status (/ca/errors/invalid_invoice_status) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_status` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El valor enviat com a estat de factura queda fora del catàleg del cicle de vida (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). ## Què fer [#què-fer] Fes servir un dels valors del catàleg, escrit exactament com el retorna l'API. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_invoice_uuid (/ca/errors/invalid_invoice_uuid) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_uuid` | `invalid_request_error` | 400 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] L'identificador de factura de la ruta o del payload no és un UUID vàlid. ## Què fer [#què-fer] Copia l'`id` exactament com el va retornar l'API, sense truncar-lo ni recodificar-lo. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_param_format (/ca/errors/invalid_param_format) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_param_format` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un form request antic va rebutjar la forma d'un valor. Els endpoints migrats reporten el mateix com a `parameter_invalid_format` o `parameter_invalid_integer`. ## Què fer [#què-fer] Corregeix el format del camp de `error.param`; si ramifiques per codi d'error, tracta aquest com a àlies de `parameter_invalid_format`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_param_value (/ca/errors/invalid_param_value) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_param_value` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un form request antic va rebutjar el valor d'un camp. Els endpoints migrats reporten el mateix com a `parameter_invalid_enum` o `parameter_invalid_range`. ## Què fer [#què-fer] Corregeix el valor de `error.param`; si ramifiques per codi d'error, tracta aquest com a àlies de `parameter_invalid_enum`. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El valor d'un o més paràmetres no és vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_payment_date (/ca/errors/invalid_payment_date) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | -------------------------------------- | | `invalid_payment_date` | `invalid_request_error` | 422 | [Pagaments](/ca/errors/index-payments) | ## Causa [#causa] La data de pagament queda fora de la finestra admesa: no pot ser anterior a la data d'emissió de la factura ni situar-se al futur. ## Què fer [#què-fer] Envia una data entre la d'emissió i avui, ambdues incloses. ## Relacionat [#relacionat] * [Tots els codis d'error de Pagaments](/ca/errors/index-payments) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_payment_method (/ca/errors/invalid_payment_method) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `invalid_payment_method` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El mètode de pagament queda fora de l'allowlist tancada: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. ## Què fer [#què-fer] Envia un d'aquests set valors; la llista és tancada i no s'amplia per empresa. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_period (/ca/errors/invalid_period) | Code | Type | HTTP | Categoria | | ---------------- | ----------------------- | ---- | ------------------------------------------------ | | `invalid_period` | `invalid_request_error` | 422 | [Informes fiscals](/ca/errors/index-tax-reports) | ## Causa [#causa] El període no identifica una declaració: l'any queda fora del rang admès, o falta el trimestre o és fora del rang 1 a 4 en un model trimestral. ## Què fer [#què-fer] Envia un any vàlid i, per al Model 303 i el Model 130, el trimestre de la declaració; el Model 347 és anual i no porta trimestre. ## Relacionat [#relacionat] * [Tots els codis d'error d'Informes fiscals](/ca/errors/index-tax-reports) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_proforma_id (/ca/errors/invalid_proforma_id) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_id` | `invalid_request_error` | 400 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] La referència de proforma rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. ## Què fer [#què-fer] Envia l'`id` que retorna l'API per a la proforma. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_proforma_number (/ca/errors/invalid_proforma_number) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_number` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] El número de proforma no segueix el format canònic de numeració de la seva sèrie. ## Què fer [#què-fer] Envia el número tal com apareix al document, amb el prefix de sèrie i l'any. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_proforma_status (/ca/errors/invalid_proforma_status) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_status` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] El valor enviat com a estat queda fora del catàleg `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. ## Què fer [#què-fer] Fes servir un dels valors del catàleg, escrit com el retorna l'API. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_proforma_uuid (/ca/errors/invalid_proforma_uuid) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_uuid` | `invalid_request_error` | 400 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] L'identificador de proforma de la ruta o del payload no és un UUID vàlid. ## Què fer [#què-fer] Copia l'`id` exactament com el va retornar l'API. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_purchase_invoice_id (/ca/errors/invalid_purchase_invoice_id) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `invalid_purchase_invoice_id` | `invalid_request_error` | 400 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] La referència de factura de compra rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. ## Què fer [#què-fer] Envia l'`id` que retorna l'API per a la factura de compra. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_purchase_invoice_number (/ca/errors/invalid_purchase_invoice_number) | Code | Type | HTTP | Categoria | | --------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `invalid_purchase_invoice_number` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] El número de factura és buit o no encaixa amb el format admès. En una factura de compra el número és el que va imprimir el proveïdor, no un que generi Factuarea. ## Què fer [#què-fer] Copia el número del document del proveïdor tal com hi apareix. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_purchase_invoice_uuid (/ca/errors/invalid_purchase_invoice_uuid) | Code | Type | HTTP | Categoria | | ------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `invalid_purchase_invoice_uuid` | `invalid_request_error` | 400 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] L'identificador de factura de compra de la ruta o del payload no és un UUID vàlid. ## Què fer [#què-fer] Copia l'`id` exactament com el va retornar l'API. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_rate_for_tax_regime (/ca/errors/invalid_rate_for_tax_regime) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_rate_for_tax_regime` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El tipus no pertany a la graella legal del seu règim: l'IGIC admet 0, 3, 5, 7, 9,5, 15 i 20 %; l'IPSI admet 0, 0,5, 1, 2, 4, 8 i 10 %. ## Què fer [#què-fer] Tria un tipus de la graella del règim; si volies un tipus d'IVA, comprova que la zona AEAT de l'impost sigui `peninsula`. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_recurring_invoice_id (/ca/errors/invalid_recurring_invoice_id) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ---------------------------------------------------------- | | `invalid_recurring_invoice_id` | `invalid_request_error` | 400 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La referència de recurrència rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. ## Què fer [#què-fer] Envia l'`id` que retorna l'API per a la recurrència. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_recurring_invoice_uuid (/ca/errors/invalid_recurring_invoice_uuid) | Code | Type | HTTP | Categoria | | -------------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `invalid_recurring_invoice_uuid` | `invalid_request_error` | 400 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] L'identificador de recurrència de la ruta o del payload no és un UUID vàlid. ## Què fer [#què-fer] Copia l'`id` exactament com el va retornar l'API. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_series_code (/ca/errors/invalid_series_code) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_code` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] El codi de la sèrie és buit, massa llarg, o porta caràcters que no corresponen a un prefix fiscal. ## Què fer [#què-fer] Envia un prefix alfanumèric curt; es desa en majúscules i passa a formar part del número de tots els documents de la sèrie. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_series_name (/ca/errors/invalid_series_name) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_name` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] El nom de la sèrie és buit o supera la longitud permesa. ## Què fer [#què-fer] Envia un nom descriptiu i breu; l'identificador fiscal és el codi, no el nom. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_series_number (/ca/errors/invalid_series_number) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_number` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] El número inicial no és vàlid: no és un enter positiu, o queda a l'últim número ja emès o per sota, cosa que reemetria números ja consumits. ## Què fer [#què-fer] Envia un número inicial per sobre del comptador actual, o omet-lo per continuar la seqüència natural. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_series_uuid (/ca/errors/invalid_series_uuid) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_uuid` | `invalid_request_error` | 400 | [Series](/ca/errors/index-series) | ## Causa [#causa] L'identificador de sèrie de la ruta o del payload no és un UUID vàlid. ## Què fer [#què-fer] Copia l'`id` exactament com el va retornar l'API. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_series_year (/ca/errors/invalid_series_year) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_year` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] L'exercici no és un any de quatre xifres vàlid per a una sèrie de numeració. ## Què fer [#què-fer] Envia l'any amb quatre xifres, corresponent a l'exercici que numera la sèrie. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_status_transition (/ca/errors/invalid_status_transition) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_status_transition` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'estat sol·licitat no és assolible des de l'estat en què es troba ara mateix el document. ## Què fer [#què-fer] Llegeix el `status` actual i recorre els passos intermedis que exigeix el cicle de vida del document abans de demanar l'estat destí. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_tax_code (/ca/errors/invalid_tax_code) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | ---------------------------------- | | `invalid_tax_code` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El codi de l'impost és buit o supera els 50 caràcters. ## Què fer [#què-fer] Envia un codi no buit de fins a 50 caràcters que identifiqui l'impost dins del teu catàleg. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_tax_name (/ca/errors/invalid_tax_name) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | ---------------------------------- | | `invalid_tax_name` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El nom de l'impost és buit o supera els 255 caràcters. ## Què fer [#què-fer] Envia un nom no buit de fins a 255 caràcters. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_tax_rate (/ca/errors/invalid_tax_rate) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | ---------------------------------- | | `invalid_tax_rate` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El tipus impositiu queda fora del rang permès per a la seva classe: IVA 0-27 %, retenció 0-47 %, recàrrec d'equivalència 0-10 %, altres 0-100 %. ## Què fer [#què-fer] Envia un tipus dins del rang de la seva classe, expressat com a percentatge i no com a fracció (`21`, no `0.21`). ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_tax_type_filter (/ca/errors/invalid_tax_type_filter) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_tax_type_filter` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El filtre `type` del llistat per tipus porta un valor fora de l'enum `vat`, `retention`, `surcharge`, `other`. ## Què fer [#què-fer] Envia un dels quatre tipus, o treu el filtre per llistar el catàleg complet. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invalid_validity_window (/ca/errors/invalid_validity_window) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_validity_window` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] La finestra de vigència està invertida: `valid_until` és anterior a `valid_from`. ## Què fer [#què-fer] Envia `valid_until` igual o posterior a `valid_from`, o omet-lo si l'impost no té data de fi. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_already_annulled (/ca/errors/invoice_already_annulled) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_already_annulled` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La factura ja estava anul·lada. L'anul·lació és terminal i, amb VeriFactu actiu, el seu registre d'anul·lació ja va arribar a l'AEAT. ## Què fer [#què-fer] No repeteixis l'anul·lació; si cal tornar a facturar l'operació, emet una factura nova. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_already_paid (/ca/errors/invoice_already_paid) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_already_paid` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La factura ja està cobrada. `paid` és un estat terminal i comptablement tancat: l'IVA repercutit ja s'ha declarat, o es declararà en el període. ## Què fer [#què-fer] Corregeix una factura pagada emetent una rectificativa que la referenciï; ja no admet edició ni anul·lació. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_already_sent (/ca/errors/invoice_already_sent) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_already_sent` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La factura ja va ser emesa: té número definitiu de sèrie i, amb VeriFactu actiu, l'alta a l'AEAT. L'emissió no passa dues vegades. ## Què fer [#què-fer] Salta't el pas d'emissió; per tornar a lliurar-la fes servir l'operació d'enviament, i per canviar-ne el contingut emet una rectificativa. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_cannot_assign_number (/ca/errors/invoice_cannot_assign_number) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_cannot_assign_number` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Es va demanar número definitiu per a una factura que no és esborrany, o que ja en té. La numeració de sèrie és monòtona i els números no es reassignen. ## Què fer [#què-fer] Demana número només sobre un esborrany que encara mostri el marcador; si la factura ja en té, llegeix-lo del camp `number`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_invalid_status_transition (/ca/errors/invoice_invalid_status_transition) | Code | Type | HTTP | Categoria | | ----------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_invalid_status_transition` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] L'estat destí no és assolible des de l'actual. El cicle de vida és dirigit: `draft` passa a `scheduled` o `sent`, `sent` a `paid`, `overdue` o `annulled`, i `paid`, `cancelled` i `annulled` són terminals. ## Què fer [#què-fer] Llegeix el `status` actual i crida l'operació del pas que necessites, en lloc de fixar l'estat destí directament. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_cancellable_in_current_state (/ca/errors/invoice_not_cancellable_in_current_state) | Code | Type | HTTP | Categoria | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_cancellable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Cancel·lar retira un esborrany que encara no és fiscalment vinculant, així que només s'aplica mentre la factura està en `draft`. ## Què fer [#què-fer] Si la factura ja està emesa, anul·la-la; si està pagada, corregeix-la amb una rectificativa. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_correctable_in_current_state (/ca/errors/invoice_not_correctable_in_current_state) | Code | Type | HTTP | Categoria | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_correctable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Una rectificativa només s'emet contra una factura ja emesa (`sent` o `paid`). Un esborrany, una factura cancel·lada o una anul·lada no tenen res a rectificar. ## Què fer [#què-fer] Emet abans la factura original; mentre segueixi en esborrany, edita-la directament en lloc de rectificar-la. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_deletable_in_current_state (/ca/errors/invoice_not_deletable_in_current_state) | Code | Type | HTTP | Categoria | | ---------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Només s'esborren les factures en `draft` i `cancelled`. Una factura numerada no desapareix mai: la sèrie correlativa ha de seguir sent auditable. ## Què fer [#què-fer] Cancel·la l'esborrany, o anul·la la factura emesa; l'esborrat no és una via per a ella. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_editable_in_current_state (/ca/errors/invoice_not_editable_in_current_state) | Code | Type | HTTP | Categoria | | --------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_editable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Només un esborrany admet edició. Un cop emesa, la factura és immutable i el seu contingut queda congelat juntament amb el seu registre fiscal. ## Què fer [#què-fer] Emet una rectificativa amb els imports correctes en lloc d'editar aquesta factura. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_eligible_for_action (/ca/errors/invoice_not_eligible_for_action) | Code | Type | HTTP | Categoria | | --------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_eligible_for_action` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] L'acció sol·licitada no s'aplica a aquesta factura: el seu tipus o el seu estat actual la deixen fora de l'abast de l'operació. ## Què fer [#què-fer] Llegeix `status` i `type` de la factura i crida l'operació que els correspon; la referència indica quins estats admet cada acció. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_found (/ca/errors/invoice_not_found) | Code | Type | HTTP | Categoria | | ------------------- | ----------------- | ---- | ------------------------------------- | | `invoice_not_found` | `not_found_error` | 404 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] L'identificador no resol a cap factura de l'empresa autenticada. Les factures d'una altra empresa responen exactament igual. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu; si només tens la teva pròpia referència, localitza la factura per `external_id` o per número de factura. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_modifiable_in_current_state (/ca/errors/invoice_not_modifiable_in_current_state) | Code | Type | HTTP | Categoria | | ----------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_modifiable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El camp que intentes canviar està congelat per a l'estat actual — per exemple el règim fiscal d'una factura anul·lada. ## Què fer [#què-fer] Llegeix `error.message` per saber quin camp hi ha implicat; en factures emeses, els canvis van per una rectificativa. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_paid (/ca/errors/invoice_not_paid) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_paid` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Es va demanar un justificant de pagament d'una factura sense cobrament registrat, així que no hi ha res a certificar. ## Què fer [#què-fer] Registra abans el cobrament i demana després el justificant. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_reschedulable_in_current_state (/ca/errors/invoice_not_reschedulable_in_current_state) | Code | Type | HTTP | Categoria | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_reschedulable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Reprogramar mou la data d'emissió d'una factura que espera en `scheduled`, i aquesta factura no està esperant. ## Què fer [#què-fer] Comprova el `status`: si és `draft`, programa-la; si ja és `sent`, l'emissió va passar i la data no es pot moure. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_schedulable_in_current_state (/ca/errors/invoice_not_schedulable_in_current_state) | Code | Type | HTTP | Categoria | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_schedulable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Només un esborrany es pot programar: la programació reserva un moment futur d'emissió sense consumir encara número de sèrie. ## Què fer [#què-fer] Programa la factura mentre segueixi en esborrany; si ja està emesa, no queda res a programar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_unschedulable_in_current_state (/ca/errors/invoice_not_unschedulable_in_current_state) | Code | Type | HTTP | Categoria | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_unschedulable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Desprogramar torna la factura de `scheduled` a `draft`, així que només s'aplica mentre segueix esperant a emetre's. ## Què fer [#què-fer] Si l'emissió programada ja es va executar, la factura està `sent`: desfés-la anul·lant-la o emetent una rectificativa. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_not_unsendable_in_current_state (/ca/errors/invoice_not_unsendable_in_current_state) | Code | Type | HTTP | Categoria | | ----------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_unsendable_in_current_state` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Desfer la marca de lliurament només s'aplica a una factura `sent`: neteja `sent_at` i manté la factura emesa. ## Què fer [#què-fer] No ho facis servir sobre factures pagades, vençudes, anul·lades o cancel·lades — aquestes demanen una rectificativa o una anul·lació, no un desfer. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_requires_at_least_one_line (/ca/errors/invoice_requires_at_least_one_line) | Code | Type | HTTP | Categoria | | ------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La factura no porta cap línia d'operació, així que no té base imposable i no es pot emetre. Passa tant quan no envies línies com quan totes les que envies són de suplert: un suplert és una quantitat pagada per compte del client (art. 78.Tres.3 LIVA), no una operació teva. ## Què fer [#què-fer] Afegeix com a mínim una línia d'operació (`line_type` NORMAL, el valor per defecte) amb descripció, quantitat i preu unitari. Si el que vols és facturar la despesa com a teva, repercuteix-la en una línia normal amb el seu tipus d'IVA en lloc de declarar-la suplert. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # invoice_year_required_for_ambiguous_number (/ca/errors/invoice_year_required_for_ambiguous_number) | Code | Type | HTTP | Categoria | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_year_required_for_ambiguous_number` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Aquest número de factura existeix en més d'un exercici, així que per si sol no identifica una única factura. ## Què fer [#què-fer] Repeteix la cerca afegint `year`; `error.message` enumera els anys en què existeix aquest número. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # ip_not_allowed (/ca/errors/ip_not_allowed) | Code | Type | HTTP | Categoria | | ---------------- | ---------------------- | ---- | ----------------------------------------------- | | `ip_not_allowed` | `authentication_error` | 401 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] La clau restringeix les adreces que accepta, i la petició va arribar des d'una que no és a la llista. ## Què fer [#què-fer] Afegeix l'adreça de sortida del teu servidor a la llista de la clau, o fes servir una clau sense restricció d'IP per a clients amb adreça canviant. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # length_required (/ca/errors/length_required) | Code | Type | HTTP | Categoria | | ----------------- | ----------------------- | ---- | ----------------------------------- | | `length_required` | `invalid_request_error` | 411 | [Request](/ca/errors/index-request) | ## Causa [#causa] Va arribar una petició amb body en codificació chunked, sense declarar-ne la mida. L'API necessita conèixer la longitud per avançat per rebutjar payloads excessius abans de carregar-los a memòria. ## Què fer [#què-fer] Envia el body amb capçalera `Content-Length` en lloc de `Transfer-Encoding: chunked`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # line_total_checksum_mismatch (/ca/errors/line_total_checksum_mismatch) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `line_total_checksum_mismatch` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El `line_total` declarat no coincideix amb el que calcula Factuarea per a aquella línia (quantitat × preu − descompte + IVA − retenció + recàrrec) i la desviació supera el cèntim de tolerància. L'import que es factura i es declara a l'AEAT és sempre el calculat aquí, així que la discrepància vol dir que el teu sistema i la factura emesa no quadrarien. ## Què fer [#què-fer] Compara `error.details.expected` (el nostre total) amb `error.details.received` (el teu) i corregeix l'arrodoniment al teu costat. El camp és un checksum opcional d'entrada que no es persisteix mai: també pots ometre'l i prendre els imports de la resposta. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # line_type_invalid (/ca/errors/line_type_invalid) | Code | Type | HTTP | Categoria | | ------------------- | ----------------------- | ---- | ------------------------------------- | | `line_type_invalid` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El tipus de línia queda fora del catàleg tancat `NORMAL` / `SUPLIDO`. Una factura emesa només distingeix dues naturaleses: el que véns tu, que forma base imposable i porta IVA, i el suplert, que són diners avançats en nom i per compte del client i per això queda fora de la base (art. 78.Tres.3 LIVA). ## Què fer [#què-fer] Envia `NORMAL` per al que factures com a propi i `SUPLIDO` només per a les quantitats que pagues a un tercer per compte del client; `error.details.allowed_values` porta el catàleg exacte. Una despesa teva que repercuteixes no és un suplert: va com a `NORMAL` amb el seu tipus d'IVA. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # maintenance (/ca/errors/maintenance) | Code | Type | HTTP | Categoria | | ------------- | --------------------------- | ---- | ----------------------------------- | | `maintenance` | `service_unavailable_error` | 503 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] La plataforma és en finestra de manteniment i les escriptures es retenen a propòsit. ## Què fer [#què-fer] Reintenta quan acabi la finestra; encua les escriptures al teu costat perquè no es perdi res mentrestant. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # max_api_keys_exceeded (/ca/errors/max_api_keys_exceeded) | Code | Type | HTTP | Categoria | | ----------------------- | --------------------- | ---- | ---------------------------------------------- | | `max_api_keys_exceeded` | `authorization_error` | 422 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] L'empresa va arribar al nombre de claus API que permet el seu pla. ## Què fer [#què-fer] Revoca les claus que ja no facis servir abans d'emetre'n una de nova, o puja de pla si de debò necessites més claus simultànies. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # max_retries_exceeded (/ca/errors/max_retries_exceeded) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | --------------------------------------- | | `max_retries_exceeded` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El registre va esgotar el pressupost de reintents tècnics de reenviament de l'XML emmagatzemat. Reintentar el mateix contingut tornaria a fallar igual. ## Què fer [#què-fer] Llegeix l'error de l'AEAT, corregeix la dada d'origen i fes servir el flux d'esmena: regenera el contingut i reinicia la ronda de reintents. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # max_webhook_endpoints_exceeded (/ca/errors/max_webhook_endpoints_exceeded) | Code | Type | HTTP | Categoria | | -------------------------------- | --------------------- | ---- | ---------------------------------------------- | | `max_webhook_endpoints_exceeded` | `authorization_error` | 422 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] L'empresa va arribar al nombre d'endpoints de webhook que permet el seu nivell d'add-on. ## Què fer [#què-fer] Elimina els endpoints que ja no escoltes, o passa a un nivell amb límit més alt; un mateix endpoint es pot subscriure a diversos tipus d'esdeveniment. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # metadata_too_many_keys (/ca/errors/metadata_too_many_keys) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `metadata_too_many_keys` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'objecte `metadata` supera el límit de 50 claus per recurs. ## Què fer [#què-fer] Redueix `metadata` a 50 claus o menys i desa la resta al teu sistema, indexada per l'`id` del recurs. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # metadata_value_too_long (/ca/errors/metadata_value_too_long) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `metadata_value_too_long` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un valor de `metadata` supera els 500 caràcters un cop serialitzat a text. ## Què fer [#què-fer] Escurça aquest valor per sota dels 500 caràcters, o desa el contingut llarg al teu sistema i deixa només una referència a `metadata`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # method_not_allowed (/ca/errors/method_not_allowed) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | ----------------------------------- | | `method_not_allowed` | `invalid_request_error` | 405 | [Request](/ca/errors/index-request) | ## Causa [#causa] La ruta existeix però no accepta el verb HTTP utilitzat. ## Què fer [#què-fer] Comprova el verb a la referència de l'endpoint; la capçalera `Allow` de la resposta llista els que admet aquesta ruta. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Mètode HTTP no permès per a aquesta ruta. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # missing_api_key (/ca/errors/missing_api_key) | Code | Type | HTTP | Categoria | | ----------------- | ---------------------- | ---- | ----------------------------------------------- | | `missing_api_key` | `authentication_error` | 401 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] La petició no porta credencials: ni capçalera `Authorization` ni `X-API-Key`. ## Què fer [#què-fer] Envia `Authorization: Bearer `; la clau viatja a la capçalera, mai a la query. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Falta la capçalera Authorization o X-API-Key. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # missing_required_param (/ca/errors/missing_required_param) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `missing_required_param` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un form request antic va detectar que faltava un camp obligatori. Els endpoints ja migrats als parsers canònics reporten el mateix com a `parameter_missing`. ## Què fer [#què-fer] Afegeix el camp que falta; si ramifiques per codi d'error, tracta aquest com a àlies de `parameter_missing`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # mode_switch_blocked_until_year_end (/ca/errors/mode_switch_blocked_until_year_end) | Code | Type | HTTP | Categoria | | ------------------------------------ | ----------------------- | ---- | --------------------------------------- | | `mode_switch_blocked_until_year_end` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El mode VeriFactu es va activar en aquest exercici i ja es va emetre com a mínim un registre de facturació. Fer marxa enrere degradaria la integritat d'una cadena ja declarada a l'AEAT. ## Què fer [#què-fer] Espera al 31 de desembre de l'any en curs; la tornada enrere només està disponible mentre l'empresa no ha emès el seu primer registre. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # module_not_available_in_sandbox (/ca/errors/module_not_available_in_sandbox) | Code | Type | HTTP | Categoria | | --------------------------------- | --------------------- | ---- | ---------------------------------------------- | | `module_not_available_in_sandbox` | `authorization_error` | 403 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] El recurs pertany a un mòdul vetat en mode test. La sandbox mai toca l'AEAT, els bancs ni cobraments reals, així que aquests mòduls queden fora a propòsit. ## Què fer [#què-fer] Prova l'operació amb una clau de producció sobre una empresa real; aquesta restricció és de l'entorn, no del pla. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # monthly_quota_exceeded (/ca/errors/monthly_quota_exceeded) | Code | Type | HTTP | Categoria | | ------------------------ | ------------------ | ---- | ------------------------------------------------- | | `monthly_quota_exceeded` | `rate_limit_error` | 429 | [Límit de peticions](/ca/errors/index-rate-limit) | ## Causa [#causa] L'empresa va esgotar la quota mensual de crides que inclou el seu pla. ## Què fer [#què-fer] Espera al cicle de facturació següent o puja de pla; mentrestant, redueix el sondeig subscrivint-te a webhooks en lloc de rellegir col·leccions. ## Relacionat [#relacionat] * [Tots els codis d'error de Límit de peticions](/ca/errors/index-rate-limit) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # monthly_requires_month_segmented_format (/ca/errors/monthly_requires_month_segmented_format) | Code | Type | HTTP | Categoria | | ----------------------------------------- | ----------------------- | ---- | --------------------------------- | | `monthly_requires_month_segmented_format` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] El comptador es reinicia cada mes però la màscara de numeració no segrega per mes, així que dos mesos arrencarien al mateix correlatiu i produirien números duplicats dins de l'any. ## Què fer [#què-fer] Afegeix el testimoni de mes a `number_format`, o canvia la política de reinici a anual o a mai. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # no_invoices_in_period (/ca/errors/no_invoices_in_period) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ------------------------------------- | | `no_invoices_in_period` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] L'operació trimestral no va trobar factures en el període demanat, així que no hi ha res a empaquetar ni a enviar. ## Què fer [#què-fer] Revisa l'any i el trimestre i tria un període amb factures emeses. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # notification_not_found (/ca/errors/notification_not_found) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------- | ---- | ----------------------------------------------- | | `notification_not_found` | `not_found_error` | 404 | [Notificacions](/ca/errors/index-notifications) | ## Causa [#causa] L'identificador no correspon a cap notificació de l'empresa autenticada, o la notificació va quedar fora de la finestra de retenció. ## Què fer [#què-fer] Llista les notificacions per obtenir un `id` vigent. ## Relacionat [#relacionat] * [Tots els codis d'error de Notificacions](/ca/errors/index-notifications) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # operation_regime_invalid (/ca/errors/operation_regime_invalid) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `operation_regime_invalid` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] El règim d'operació queda fora del catàleg `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. ## Què fer [#què-fer] Tria el règim que correspon a l'operació: decideix com es declara l'IVA i si s'aplica la inversió del subjecte passiu. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # origin_not_allowed (/ca/errors/origin_not_allowed) | Code | Type | HTTP | Categoria | | -------------------- | ---------------------- | ---- | ----------------------------------------------- | | `origin_not_allowed` | `authentication_error` | 401 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] La petició ve d'un origen de navegador que la clau no accepta. ## Què fer [#què-fer] Afegeix l'origen a la configuració de la clau, o mou la crida al teu servidor: una clau API mai ha de quedar exposada en un navegador. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # pack_in_use (/ca/errors/pack_in_use) | Code | Type | HTTP | Categoria | | ------------- | ----------------------- | ---- | -------------------------------------- | | `pack_in_use` | `invalid_request_error` | 422 | [Productes](/ca/errors/index-products) | ## Causa [#causa] El pack està referenciat per documents emesos, així que esborrar-lo trencaria la seva composició. ## Què fer [#què-fer] Desactiva el pack en lloc d'esborrar-lo, o treu el producte del pack si era això el que volies canviar. ## Relacionat [#relacionat] * [Tots els codis d'error de Productes](/ca/errors/index-products) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # pack_not_found (/ca/errors/pack_not_found) | Code | Type | HTTP | Categoria | | ---------------- | ----------------- | ---- | -------------------------------------- | | `pack_not_found` | `not_found_error` | 404 | [Productes](/ca/errors/index-products) | ## Causa [#causa] L'identificador no resol a cap pack de l'empresa autenticada. ## Què fer [#què-fer] Llista els packs i fes servir l'`id` que retornen. ## Relacionat [#relacionat] * [Tots els codis d'error de Productes](/ca/errors/index-products) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # pack_share_link_failed (/ca/errors/pack_share_link_failed) | Code | Type | HTTP | Categoria | | ------------------------ | ----------- | ---- | -------------------------------------- | | `pack_share_link_failed` | `api_error` | 500 | [Productes](/ca/errors/index-products) | ## Causa [#causa] No es va poder generar l'enllaç per compartir el pack. El pack en si no queda afectat. ## Què fer [#què-fer] Reintenta al cap d'uns segons i comunica el `request_id` si segueix fallant. ## Relacionat [#relacionat] * [Tots els codis d'error de Productes](/ca/errors/index-products) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid (/ca/errors/parameter_invalid) | Code | Type | HTTP | Categoria | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un value object construït a partir del payload va rebutjar el valor rebut. `error.subcode` diu quin: codi d'impost, codi de país, tipus impositiu, etc. ## Què fer [#què-fer] Corregeix el camp que indica `error.param` seguint el format del concepte que anomena `error.subcode`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_boolean (/ca/errors/parameter_invalid_boolean) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_boolean` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un paràmetre que ha de ser booleà va rebre un valor fora de les representacions acceptades (`true`/`false`, `1`/`0`). ## Què fer [#què-fer] Envia `true` o `false` al paràmetre que indica `error.param`. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El paràmetre ha de ser un valor booleà. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_cursor (/ca/errors/parameter_invalid_cursor) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_cursor` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] El cursor `starting_after` o `ending_before` no és un UUID vàlid, així que no pot apuntar a cap fila de la col·lecció. ## Què fer [#què-fer] Fes servir com a cursor l'`id` de l'últim objecte de la pàgina anterior (o del primer, per a `ending_before`), copiat literalment de la resposta. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_empty (/ca/errors/parameter_invalid_empty) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_empty` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un paràmetre va arribar amb el valor buit: un filtre `in` sense elements, una comparació sense res després de l'operador, o un filtre d'igualtat amb la cadena buida. ## Què fer [#què-fer] Envia un valor no buit al paràmetre de `error.param`, o treu el paràmetre de la petició. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_enum (/ca/errors/parameter_invalid_enum) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_enum` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] El valor queda fora del conjunt tancat que accepta el paràmetre. En els llistats cobreix a més un operador de filtre diferent de `eq`, `gte`, `lte`, `gt`, `lt`, `in` o `contains`. ## Què fer [#què-fer] Tria un dels valors documentats per a aquest paràmetre, o un dels operadors de filtre admesos. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El valor del paràmetre no és cap dels permesos. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_format (/ca/errors/parameter_invalid_format) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_format` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] El valor té el tipus correcte però no la forma que exigeix el paràmetre: una data, un patró d'identificador o una capçalera com `Factuarea-Version`. ## Què fer [#què-fer] Reescriu el valor de `error.param` amb el patró documentat per a aquest camp i repeteix la crida. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El format del paràmetre no és vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_integer (/ca/errors/parameter_invalid_integer) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_integer` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un paràmetre que ha de ser un nombre enter va rebre alguna cosa que no es pot interpretar com a tal, per exemple `limit=abc`. ## Què fer [#què-fer] Envia el paràmetre de `error.param` com a enter en base 10, sense decimals, separadors de milers ni cometes. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El paràmetre ha de ser un nombre enter. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_iso8601 (/ca/errors/parameter_invalid_iso8601) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_iso8601` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un filtre de rang (`gte`, `lte`, `gt`, `lt`) va rebre un valor que no és numèric ni una data ISO 8601. ## Què fer [#què-fer] Envia les dates com a `YYYY-MM-DD`, o en ISO 8601 complet amb zona horària (`2026-01-31T23:59:59Z`). ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_range (/ca/errors/parameter_invalid_range) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_range` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un paràmetre numèric va quedar fora dels seus límits. El cas habitual és `limit`, que ha d'estar entre 1 i 100. ## Què fer [#què-fer] Envia un valor dins dels límits documentats; per llegir més de 100 objectes, pagina amb `starting_after`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_string (/ca/errors/parameter_invalid_string) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_string` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un paràmetre que ha de ser text va rebre un array, un objecte o un valor que no es pot llegir com a cadena. ## Què fer [#què-fer] Envia el paràmetre de `error.param` com una cadena de text simple. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El paràmetre ha de ser una cadena de text. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_url (/ca/errors/parameter_invalid_url) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_url` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un camp que ha de contenir una URL absoluta va rebre un valor que no ho és, normalment perquè li falta l'esquema o l'amfitrió. ## Què fer [#què-fer] Envia una URL absoluta `https://` al camp que indica `error.param`. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El paràmetre ha de ser una URL vàlida. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_uuid (/ca/errors/parameter_invalid_uuid) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_uuid` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un camp d'identificador va rebre un valor que no és un UUID vàlid. Tot `id` de recurs a v1 és un UUID. ## Què fer [#què-fer] Fes servir l'`id` que va retornar l'API per a aquest recurs, copiat literalment; mai un id numèric intern ni un valor truncat. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El paràmetre ha de ser un UUID vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_invalid_value (/ca/errors/parameter_invalid_value) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_value` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] El valor és sintàcticament correcte però no admissible per a aquest recurs: fora del catàleg canònic del camp, o incoherent amb la resta del payload. ## Què fer [#què-fer] Llegeix `error.param` i `error.subcode`: entre tots dos identifiquen el camp i la regla concreta que incompleix el valor. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El valor del paràmetre no és vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_missing (/ca/errors/parameter_missing) | Code | Type | HTTP | Categoria | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_missing` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'endpoint exigeix un paràmetre que la petició no portava. `error.param` diu quin. ## Què fer [#què-fer] Afegeix el paràmetre indicat a `error.param` a la query o al body i repeteix la crida. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Falta un paràmetre obligatori. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # parameter_unknown (/ca/errors/parameter_unknown) | Code | Type | HTTP | Categoria | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_unknown` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] La petició porta un paràmetre que l'endpoint no accepta: un filtre fora de la seva allowlist, un camp de `sort` no ordenable, o el `page` de paginació per offset — v1 pagina per cursor. ## Què fer [#què-fer] Treu el paràmetre que indica `error.param`; per recórrer una col·lecció fes servir `limit` juntament amb `starting_after` o `ending_before`. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # payload_too_large (/ca/errors/payload_too_large) | Code | Type | HTTP | Categoria | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `payload_too_large` | `invalid_request_error` | 413 | [Request](/ca/errors/index-request) | ## Causa [#causa] El body de la petició supera la mida admesa: 1 MB amb caràcter general, 6 MB als endpoints que accepten fitxers. ## Què fer [#què-fer] Parteix l'operació en peticions més petites, o comprimeix l'adjunt abans d'enviar-lo. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > La càrrega supera el límit d'1 MB. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # payment_method_invalid (/ca/errors/payment_method_invalid) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `payment_method_invalid` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La mateixa allowlist tancada que `invalid_payment_method`, reportada quan el valor es rebutja en llegir el camp de mètode de pagament del payload. ## Què fer [#què-fer] Envia un dels set mètodes admesos, en minúscules i amb guió baix, com `sepa_direct_debit`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # payment_method_required (/ca/errors/payment_method_required) | Code | Type | HTTP | Categoria | | ------------------------- | ------------------------ | ---- | -------------------------------------- | | `payment_method_required` | `payment_required_error` | 402 | [Empreses](/ca/errors/index-companies) | ## Causa [#causa] Donar d'alta una empresa gestionada cobra un seient immediatament, i la gestoria opera en mode real sense mètode de pagament configurat. ## Què fer [#què-fer] Obre el portal de facturació a `error.details.payment_setup_url`, registra un mètode de pagament i repeteix la mateixa crida. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empreses](/ca/errors/index-companies) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # payout_reconciliation_amount_mismatch (/ca/errors/payout_reconciliation_amount_mismatch) | Code | Type | HTTP | Categoria | | --------------------------------------- | ----------------------- | ---- | -------------------------------------- | | `payout_reconciliation_amount_mismatch` | `invalid_request_error` | 422 | [Pagaments](/ca/errors/index-payments) | ## Causa [#causa] L'import confirmat no coincideix amb el net de la liquidació, així que la conciliació tancaria amb una diferència que ningú justifica. ## Què fer [#què-fer] Concilia contra l'import net —brut menys comissions d'Stripe— i comprova que el moviment bancari correspon a aquesta liquidació. ## Relacionat [#relacionat] * [Tots els codis d'error de Pagaments](/ca/errors/index-payments) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # pdf_generation_failed (/ca/errors/pdf_generation_failed) | Code | Type | HTTP | Categoria | | ----------------------- | --------------------------- | ---- | ----------------------------------- | | `pdf_generation_failed` | `service_unavailable_error` | 503 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] El servei de renderitzat no va poder produir el PDF. El document i les seves dades són intactes: el que ha fallat és el fitxer. ## Què fer [#què-fer] Reintenta al cap d'uns segons; si persisteix, comunica el `request_id` a suport i comparteix mentrestant el document pel seu enllaç públic. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # product_in_use (/ca/errors/product_in_use) | Code | Type | HTTP | Categoria | | ---------------- | ----------------------- | ---- | -------------------------------------- | | `product_in_use` | `invalid_request_error` | 422 | [Productes](/ca/errors/index-products) | ## Causa [#causa] El producte està referenciat per documents emesos o per altres entrades del catàleg, i eliminar-lo deixaria aquestes referències penjant. ## Què fer [#què-fer] Desactiva el producte en lloc d'esborrar-lo: deixa d'oferir-se i els documents que el van fer servir segueixen intactes. ## Relacionat [#relacionat] * [Tots els codis d'error de Productes](/ca/errors/index-products) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # product_not_found (/ca/errors/product_not_found) | Code | Type | HTTP | Categoria | | ------------------- | ----------------- | ---- | -------------------------------------- | | `product_not_found` | `not_found_error` | 404 | [Productes](/ca/errors/index-products) | ## Causa [#causa] L'identificador no resol a cap producte de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id`, o busca el producte pel seu SKU o el seu `external_id` abans de crear un duplicat. ## Relacionat [#relacionat] * [Tots els codis d'error de Productes](/ca/errors/index-products) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # profile_not_found (/ca/errors/profile_not_found) | Code | Type | HTTP | Categoria | | ------------------- | ----------------- | ---- | ----------------------------------- | | `profile_not_found` | `not_found_error` | 404 | [Request](/ca/errors/index-request) | ## Causa [#causa] La capçalera `X-Active-Profile` anomena una empresa que no existeix o que no pertany a l'arbre de gestoria de la clau autenticada. Tots dos casos responen igual perquè l'API mai reveli empreses d'altres tenants. ## Què fer [#què-fer] Envia l'`id` d'una de les empreses gestionades que llista `GET /v1/companies`, o treu la capçalera per operar sobre la teva pròpia empresa. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_already_accepted (/ca/errors/proforma_already_accepted) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_already_accepted` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] El client ja va acceptar la proforma, i l'acceptació es registra una sola vegada. ## Què fer [#què-fer] Passa a la conversió en factura; no queda res a acceptar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_already_rejected (/ca/errors/proforma_already_rejected) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_already_rejected` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] La proforma ja està marcada com a rebutjada. ## Què fer [#què-fer] Si el client ha canviat d'opinió, registra l'acceptació: una proforma rebutjada encara es pot acceptar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_cannot_be_accepted (/ca/errors/proforma_cannot_be_accepted) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_cannot_be_accepted` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] L'acceptació no escau des de l'estat actual: una proforma facturada, cancel·lada o expirada ja no l'admet. ## Què fer [#què-fer] Emet una proforma nova amb les condicions vigents i fes que s'accepti aquesta. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_cannot_be_rejected (/ca/errors/proforma_cannot_be_rejected) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_cannot_be_rejected` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] El rebuig no escau des de l'estat actual: un cop facturada, cancel·lada o expirada, la proforma està tancada. ## Què fer [#què-fer] Si l'operació no tira endavant i la proforma ja es va facturar, corregeix la factura en lloc de rebutjar la proforma. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_cannot_be_sent (/ca/errors/proforma_cannot_be_sent) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_cannot_be_sent` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] L'enviament per correu no s'aplica a una proforma en estat terminal: no hi ha oferta viva a lliurar. ## Què fer [#què-fer] Emet una proforma nova i envia aquesta; un document tancat només es comparteix com a descàrrega. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_invalid_status_transition (/ca/errors/proforma_invalid_status_transition) | Code | Type | HTTP | Categoria | | ------------------------------------ | ----------------------- | ---- | ----------------------------------------------- | | `proforma_invalid_status_transition` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] L'estat destí no és assolible des de l'actual: un esborrany s'accepta, es cancel·la o expira; una proforma acceptada es factura, es rebutja o expira; facturada, cancel·lada i expirada són terminals. ## Què fer [#què-fer] Llegeix el `status` actual i recorre el pas intermedi que exigeix el cicle de vida. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_not_convertible_in_current_state (/ca/errors/proforma_not_convertible_in_current_state) | Code | Type | HTTP | Categoria | | ------------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_convertible_in_current_state` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] Convertir en factura exigeix que el client hagi acceptat la proforma; des de qualsevol altre estat no hi ha acord a facturar. ## Què fer [#què-fer] Registra abans l'acceptació i converteix després; si el client no la va acceptar mai, emet la factura directament. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_not_deletable_in_current_state (/ca/errors/proforma_not_deletable_in_current_state) | Code | Type | HTTP | Categoria | | ----------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] Només s'esborra una proforma en esborrany. Un cop acceptada, rebutjada o facturada forma part del rastre comercial. ## Què fer [#què-fer] Cancel·la la proforma en lloc d'esborrar-la: la cancel·lació conserva l'històric i la retira de circulació. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_not_draft (/ca/errors/proforma_not_draft) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_draft` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] L'operació només té sentit mentre la proforma és un esborrany, i aquesta ja ha avançat. ## Què fer [#què-fer] Llegeix el `status` i fes servir l'operació que li correspon, o parteix d'un esborrany nou. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_not_editable_in_current_state (/ca/errors/proforma_not_editable_in_current_state) | Code | Type | HTTP | Categoria | | ---------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_editable_in_current_state` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] Només una proforma en esborrany admet edició. Un cop acceptada, rebutjada, expirada, facturada o cancel·lada, el seu contingut queda fixat. ## Què fer [#què-fer] Duplica la proforma per treballar sobre un esborrany nou, en lloc d'editar la que ja està tancada. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_not_found (/ca/errors/proforma_not_found) | Code | Type | HTTP | Categoria | | -------------------- | ----------------- | ---- | ----------------------------------------------- | | `proforma_not_found` | `not_found_error` | 404 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] L'identificador no resol a cap proforma de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu, o localitza la proforma pel seu `external_id`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # proforma_requires_at_least_one_line (/ca/errors/proforma_requires_at_least_one_line) | Code | Type | HTTP | Categoria | | ------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_requires_at_least_one_line` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] La proforma no porta línies, així que no hi ha import a posar davant del client. ## Què fer [#què-fer] Afegeix com a mínim una línia amb descripció, quantitat i preu unitari. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # public_link_expires_at_exceeds_max_days (/ca/errors/public_link_expires_at_exceeds_max_days) | Code | Type | HTTP | Categoria | | ----------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `public_link_expires_at_exceeds_max_days` | `invalid_request_error` | 422 | [Factures proforma](/ca/errors/index-proformas) | ## Causa [#causa] La caducitat demanada per a l'enllaç públic supera la finestra màxima que permet el teu pla per a documents compartits. ## Què fer [#què-fer] Envia un `expires_at` més proper; quan l'enllaç caduqui el pots renovar tantes vegades com calgui. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures proforma](/ca/errors/index-proformas) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # purchase_invoice_already_exists (/ca/errors/purchase_invoice_already_exists) | Code | Type | HTTP | Categoria | | --------------------------------- | ---------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_already_exists` | `conflict_error` | 409 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] Aquest proveïdor ja té registrada una factura de compra amb el mateix número. El parell proveïdor + número identifica el document sense ambigüitat i evita comptabilitzar dues vegades la mateixa despesa. ## Què fer [#què-fer] Actualitza la factura existent en lloc de tornar-la a registrar, o revisa el número si el proveïdor va emetre realment dos documents. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # purchase_invoice_not_deletable_in_current_state (/ca/errors/purchase_invoice_not_deletable_in_current_state) | Code | Type | HTTP | Categoria | | ------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] Només s'esborren les factures de compra en esborrany o cancel·lades. Una de pendent o pagada forma part del llibre de despeses. ## Què fer [#què-fer] Cancel·la la factura en lloc d'esborrar-la; un cop cancel·lada sí que es pot eliminar si de debò no la vols deixar registrada. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # purchase_invoice_not_draft (/ca/errors/purchase_invoice_not_draft) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_draft` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] L'operació només s'aplica mentre la factura de compra és un esborrany, i aquesta ja està registrada. ## Què fer [#què-fer] Llegeix el `status` i fes servir l'operació que li correspon: les factures registrades canvien per pagament o per cancel·lació, no per edició d'esborrany. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # purchase_invoice_not_editable_in_current_state (/ca/errors/purchase_invoice_not_editable_in_current_state) | Code | Type | HTTP | Categoria | | ------------------------------------------------ | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_editable_in_current_state` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] Només s'edita una factura de compra en esborrany. Un cop registrada com a pendent, pagada o cancel·lada, el seu contingut dona suport a un apunt comptable. ## Què fer [#què-fer] Torna-la a esborrany si encara està pendent, o registra la diferència amb un document nou si ja està liquidada. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # purchase_invoice_not_found (/ca/errors/purchase_invoice_not_found) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_found` | `not_found_error` | 404 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] L'identificador no resol a cap factura de compra de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu, o localitza la factura pel seu `external_id`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # purchase_invoice_requires_at_least_one_line (/ca/errors/purchase_invoice_requires_at_least_one_line) | Code | Type | HTTP | Categoria | | --------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Factures de compra](/ca/errors/index-purchase-invoices) | ## Causa [#causa] La factura de compra no porta línies, així que no hi ha despesa ni IVA suportat a registrar. ## Què fer [#què-fer] Afegeix com a mínim una línia amb descripció, quantitat i preu unitari abans de desar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures de compra](/ca/errors/index-purchase-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # quote_already_accepted (/ca/errors/quote_already_accepted) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `quote_already_accepted` | `invalid_request_error` | 422 | [Pressupostos](/ca/errors/index-quotes) | ## Causa [#causa] El pressupost ja estava aprovat, i l'aprovació es registra una sola vegada. ## Què fer [#què-fer] Passa a la conversió en factura; no queda res a aprovar. ## Relacionat [#relacionat] * [Tots els codis d'error de Pressupostos](/ca/errors/index-quotes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # quote_already_rejected (/ca/errors/quote_already_rejected) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `quote_already_rejected` | `invalid_request_error` | 422 | [Pressupostos](/ca/errors/index-quotes) | ## Causa [#causa] El pressupost ja està marcat com a rebutjat. ## Què fer [#què-fer] Si el client ha canviat d'opinió, registra l'aprovació: un pressupost rebutjat encara es pot aprovar. ## Relacionat [#relacionat] * [Tots els codis d'error de Pressupostos](/ca/errors/index-quotes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # quote_expired (/ca/errors/quote_expired) | Code | Type | HTTP | Categoria | | --------------- | ----------------------- | ---- | --------------------------------------- | | `quote_expired` | `invalid_request_error` | 422 | [Pressupostos](/ca/errors/index-quotes) | ## Causa [#causa] El pressupost va passar la seva data de validesa, així que les condicions ofertes ja no vinculen i no es pot aprovar ni convertir tal com està. ## Què fer [#què-fer] Duplica el pressupost amb una data de validesa nova i fes que el client aprovi aquest. ## Relacionat [#relacionat] * [Tots els codis d'error de Pressupostos](/ca/errors/index-quotes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # quote_not_found (/ca/errors/quote_not_found) | Code | Type | HTTP | Categoria | | ----------------- | ----------------- | ---- | --------------------------------------- | | `quote_not_found` | `not_found_error` | 404 | [Pressupostos](/ca/errors/index-quotes) | ## Causa [#causa] L'identificador no resol a cap pressupost de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu, o localitza el pressupost pel seu `external_id`. ## Relacionat [#relacionat] * [Tots els codis d'error de Pressupostos](/ca/errors/index-quotes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # rate_limit_exceeded (/ca/errors/rate_limit_exceeded) | Code | Type | HTTP | Categoria | | --------------------- | ------------------ | ---- | ------------------------------------------------- | | `rate_limit_exceeded` | `rate_limit_error` | 429 | [Límit de peticions](/ca/errors/index-rate-limit) | ## Causa [#causa] La clau va enviar més peticions de les que permet el seu ritme a la finestra actual. ## Què fer [#què-fer] Llegeix la capçalera `Retry-After` i espera aquest temps; reparteix la feina massiva i fes servir els endpoints en lot en lloc d'una crida per objecte. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Has superat el límit de peticions permès. Torna-ho a provar més tard. ## Relacionat [#relacionat] * [Tots els codis d'error de Límit de peticions](/ca/errors/index-rate-limit) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # receipt_not_available (/ca/errors/receipt_not_available) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | -------------------------------------- | | `receipt_not_available` | `invalid_request_error` | 422 | [Pagaments](/ca/errors/index-payments) | ## Causa [#causa] No hi ha justificant a emetre perquè el document no té cap cobrament registrat al darrere. ## Què fer [#què-fer] Registra abans el cobrament; el justificant certifica un pagament que ja existeix. ## Relacionat [#relacionat] * [Tots els codis d'error de Pagaments](/ca/errors/index-payments) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # record_already_accepted (/ca/errors/record_already_accepted) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | --------------------------------------- | | `record_already_accepted` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'AEAT ja va acceptar el registre. L'acceptació és terminal i el seu contingut queda congelat com a part de la cadena d'empremtes. ## Què fer [#què-fer] Per corregir una factura acceptada, emet una rectificativa: un registre acceptat no es reenvia ni s'esmena. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # record_immutable (/ca/errors/record_immutable) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | --------------------------------------- | | `record_immutable` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El registre pertany a un ledger de només-addició: un cop escrit, el seu contingut fiscal queda tancat a modificacions i a esborrat. ## Què fer [#què-fer] Afegeix un registre nou que el corregeixi — anul·lació més alta nova, o rectificativa — en lloc d'editar l'existent. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # record_not_rejected (/ca/errors/record_not_rejected) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------------- | | `record_not_rejected` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'esmena només s'aplica a registres que l'AEAT va rebutjar per dades. Aquest registre està en un altre estat — una fallada tècnica, per exemple, la cobreix el reintent automàtic. ## Què fer [#què-fer] Reintenta la transmissió si la fallada va ser tècnica; si l'AEAT va acceptar el registre, corregeix la factura amb una rectificativa. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # record_not_subsanable (/ca/errors/record_not_subsanable) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | --------------------------------------- | | `record_not_subsanable` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El registre no es pot esmenar: no és un registre d'alta, o no té factura d'origen des de la qual regenerar-ne el contingut. ## Què fer [#què-fer] Fes servir una anul·lació més una alta nova, o emet una rectificativa, segons què s'hagi de canviar. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_already_active (/ca/errors/recurring_already_active) | Code | Type | HTTP | Categoria | | -------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_already_active` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència ja està en marxa, així que no hi ha res a activar. Codi antic conservat per compatibilitat: els endpoints actuals reporten això com a `recurring_invoice_already_active`. ## Què fer [#què-fer] Llegeix `status` abans d'actuar; per canviar la programació, actualitza la recurrència en lloc de tornar-la a activar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_already_active (/ca/errors/recurring_invoice_already_active) | Code | Type | HTTP | Categoria | | ---------------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_already_active` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència ja està en marxa. ## Què fer [#què-fer] Llegeix `status` abans d'actuar; per canviar quan s'executa la propera vegada, actualitza la recurrència. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_already_cancelled (/ca/errors/recurring_invoice_already_cancelled) | Code | Type | HTTP | Categoria | | ------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_already_cancelled` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència ja estava cancel·lada, i la cancel·lació és terminal. ## Què fer [#què-fer] Crea una recurrència nova si necessites tornar a facturar periòdicament aquest client. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_already_paused (/ca/errors/recurring_invoice_already_paused) | Code | Type | HTTP | Categoria | | ---------------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_already_paused` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència ja està pausada, així que pausar-la un altre cop no canvia res. ## Què fer [#què-fer] Llegeix `status` abans d'actuar; per recuperar-la fes servir l'operació de reprendre. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_cancelled_cannot_resume (/ca/errors/recurring_invoice_cancelled_cannot_resume) | Code | Type | HTTP | Categoria | | ------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_cancelled_cannot_resume` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] Una recurrència cancel·lada no es reprèn: la cancel·lació la tanca definitivament, a diferència de la pausa. ## Què fer [#què-fer] Duplica-la en una recurrència nova, o fes servir la pausa en lloc de la cancel·lació quan l'aturada sigui temporal. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_cannot_run (/ca/errors/recurring_invoice_cannot_run) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_cannot_run` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència no pot generar una factura ara mateix: no està en marxa, el seu cicle s'ha acabat, o li falten dades que la factura necessita. `error.message` indica el motiu concret. ## Què fer [#què-fer] Arregla el que indica el missatge —reprendre-la, ampliar el nombre d'ocurrències o completar les dades que falten— abans de forçar una execució. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_has_generated_invoices (/ca/errors/recurring_invoice_has_generated_invoices) | Code | Type | HTTP | Categoria | | ------------------------------------------ | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_has_generated_invoices` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència ja va generar factures, i aquestes factures en depenen per a la seva traçabilitat. ## Què fer [#què-fer] Cancel·la la recurrència en lloc d'esborrar-la: deixa de generar i les factures ja emeses conserven el seu origen. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_not_found (/ca/errors/recurring_invoice_not_found) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_not_found` | `not_found_error` | 404 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] L'identificador no resol a cap recurrència de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu, o localitza la recurrència pel seu `external_id`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_invoice_requires_at_least_one_line (/ca/errors/recurring_invoice_requires_at_least_one_line) | Code | Type | HTTP | Categoria | | ---------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] La recurrència no porta línies, així que cada factura generada sortiria buida. ## Què fer [#què-fer] Afegeix com a mínim una línia amb descripció, quantitat i preu unitari abans de desar o activar la recurrència. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # recurring_not_active (/ca/errors/recurring_not_active) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ---------------------------------------------------------- | | `recurring_not_active` | `invalid_request_error` | 422 | [Factures recurrents](/ca/errors/index-recurring-invoices) | ## Causa [#causa] L'operació necessita una recurrència en marxa i aquesta està pausada, completada o cancel·lada. Codi antic conservat per compatibilitat amb integracions velles. ## Què fer [#què-fer] Reprèn la recurrència abans de l'operació, o llegeix `status` per veure per què es va aturar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures recurrents](/ca/errors/index-recurring-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # register_sealing_failed (/ca/errors/register_sealing_failed) | Code | Type | HTTP | Categoria | | ------------------------- | ----------- | ---- | ----------------------------------- | | `register_sealing_failed` | `api_error` | 500 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] El segellat criptogràfic del registre no es va completar, així que el tancament va quedar sense signar en lloc de segellat amb una signatura trencada. ## Què fer [#què-fer] Revisa el certificat de signatura de l'empresa i repeteix el tancament; comunica el `request_id` si la fallada es repeteix amb un certificat vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # reminder_not_applicable (/ca/errors/reminder_not_applicable) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ------------------------------------- | | `reminder_not_applicable` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] El recordatori de pagament no escau: la factura no està en `sent` ni `overdue`, no hi ha adreça de destinatari, falta l'enllaç públic o està desactivat, o ja va sortir un altre recordatori les últimes 24 hores. ## Què fer [#què-fer] Revisa l'estat, activa l'enllaç públic, indica una adreça de destinatari i respecta l'espera de 24 hores abans de reintentar. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # replay_delivery_not_retryable (/ca/errors/replay_delivery_not_retryable) | Code | Type | HTTP | Categoria | | ------------------------------- | ----------------------- | ---- | ------------------------------------- | | `replay_delivery_not_retryable` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] Només es reenvien els lliuraments fallits. Un lliurament que va arribar bé, o un encara en curs, no té res a reenviar. ## Què fer [#què-fer] Llegeix el `status` del lliurament: el reenviament s'aplica als fallits; per a un lliurament correcte, torna a llegir l'esdeveniment. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # replay_event_expired (/ca/errors/replay_event_expired) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `replay_event_expired` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] L'esdeveniment que dona suport al lliurament va ser purgat per la política de retenció de 30 dies, així que ja no queda payload a reenviar. ## Què fer [#què-fer] Reconstrueix l'estat des del recurs afectat a través del seu endpoint; els esdeveniments de més de 30 dies no es recuperen. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # report_format_invalid (/ca/errors/report_format_invalid) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ------------------------------------------------ | | `report_format_invalid` | `invalid_request_error` | 422 | [Informes fiscals](/ca/errors/index-tax-reports) | ## Causa [#causa] El format queda fora del catàleg `txt_aeat`, `pdf`, `excel`. ## Què fer [#què-fer] Envia `txt_aeat` per presentar davant l'AEAT, `pdf` per a una còpia llegible, o `excel` per treballar sobre les xifres. ## Relacionat [#relacionat] * [Tots els codis d'error d'Informes fiscals](/ca/errors/index-tax-reports) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # requires_annulment (/ca/errors/requires_annulment) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | --------------------------------------- | | `requires_annulment` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El contingut regenerat canvia un camp que entra a l'empremta —NIF de l'emissor, sèrie i número, data d'expedició, tipus de factura, quota o import total— i la cadena no es pot reescriure. ## Què fer [#què-fer] Anul·la el registre i dona d'alta una factura nova, o una rectificativa, amb les dades correctes. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # resource_already_exists (/ca/errors/resource_already_exists) | Code | Type | HTTP | Categoria | | ------------------------- | ---------------- | ---- | ----------------------------------- | | `resource_already_exists` | `conflict_error` | 409 | [Request](/ca/errors/index-request) | ## Causa [#causa] Crear l'objecte duplicaria un que ja existeix sota una clau única — NIF, SKU, external id. `error.details.existing_resource_id` apunta a l'objecte que ja ocupa aquest valor. ## Què fer [#què-fer] Actualitza l'objecte que retorna `existing_resource_id`, o envia un altre valor al camp que ha de ser únic. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # resource_conflict (/ca/errors/resource_conflict) | Code | Type | HTTP | Categoria | | ------------------- | ---------------- | ---- | ----------------------------------- | | `resource_conflict` | `conflict_error` | 409 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'operació va xocar amb l'estat actual del recurs i no s'aplica cap codi de conflicte més específic. ## Què fer [#què-fer] Torna a llegir el recurs, aplica el teu canvi sobre l'estat que acabes de llegir i repeteix l'operació. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # resource_immutable (/ca/errors/resource_immutable) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | ----------------------------------- | | `resource_immutable` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'objecte està tancat a canvis per a aquesta operació: el seu estat o el seu registre comptable impedeixen modificar-lo. ## Què fer [#què-fer] Llegeix `error.subcode` per saber quina regla el va tancar; la via habitual és emetre un document nou en lloc d'editar aquest. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # resource_locked (/ca/errors/resource_locked) | Code | Type | HTTP | Categoria | | ----------------- | ---------------- | ---- | ----------------------------------- | | `resource_locked` | `conflict_error` | 409 | [Request](/ca/errors/index-request) | ## Causa [#causa] Una altra operació reté el recurs fins que acaba: les escriptures concurrents sobre el mateix objecte se serialitzen en lloc d'entrellaçar-se. ## Què fer [#què-fer] Reintenta després d'una espera breu i reutilitza la mateixa `Idempotency-Key`, perquè el reintent no pugui duplicar l'escriptura. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # resource_not_deletable (/ca/errors/resource_not_deletable) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `resource_not_deletable` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'objecte existeix, però el seu estat o els seus dependents bloquegen l'esborrat. En els esborrats massius aquest és el codi per fila de cada entrada que no es va poder eliminar. ## Què fer [#què-fer] Llegeix el `reason` de cada fila fallida, elimina o reassigna els dependents, i repeteix l'esborrat només per a aquestes files. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # resource_not_found (/ca/errors/resource_not_found) | Code | Type | HTTP | Categoria | | -------------------- | ----------------- | ---- | ----------------------------------- | | `resource_not_found` | `not_found_error` | 404 | [Request](/ca/errors/index-request) | ## Causa [#causa] L'identificador no resol a res visible per a l'empresa autenticada. Els objectes d'una altra empresa responen exactament igual, a propòsit. ## Què fer [#què-fer] Revisa l'`id` i el perfil actiu (`X-Active-Profile`); llista la col·lecció per confirmar que l'objecte existeix per a aquesta empresa. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > El recurs sol·licitat no existeix. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # route_not_found (/ca/errors/route_not_found) | Code | Type | HTTP | Categoria | | ----------------- | ----------------- | ---- | ----------------------------------- | | `route_not_found` | `not_found_error` | 404 | [Request](/ca/errors/index-request) | ## Causa [#causa] La ruta no correspon a cap endpoint de v1. Sol ser una errada, un prefix `/v1` absent o una ruta d'una altra àrea de l'API. ## Què fer [#què-fer] Comprova la ruta a la referència de l'API, inclosa la URL base (`https://api.factuarea.com/v1`). ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Recurs no trobat. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # scheduled_for_in_past (/ca/errors/scheduled_for_in_past) | Code | Type | HTTP | Categoria | | ----------------------- | ----------------------- | ---- | ------------------------------------- | | `scheduled_for_in_past` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] `scheduled_for` no és estrictament futur, així que no hi ha cap espera a reservar. ## Què fer [#què-fer] Envia `scheduled_for` com un instant posterior a ara, en ISO 8601 amb zona horària; per emetre ja, fes servir l'operació d'emissió. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # scope_not_allowed_by_plan (/ca/errors/scope_not_allowed_by_plan) | Code | Type | HTTP | Categoria | | --------------------------- | --------------------- | ---- | ---------------------------------------------- | | `scope_not_allowed_by_plan` | `authorization_error` | 422 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] Un dels abasts demanats pertany a un mòdul que el pla no inclou, així que la clau naixeria amb un permís que mai podria exercir. ## Què fer [#què-fer] Emet la clau sense aquest abast, o puja de pla abans d'incloure'l. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # scope_not_allowed_in_sandbox (/ca/errors/scope_not_allowed_in_sandbox) | Code | Type | HTTP | Categoria | | ------------------------------ | --------------------- | ---- | ---------------------------------------------- | | `scope_not_allowed_in_sandbox` | `authorization_error` | 422 | [Autorització](/ca/errors/index-authorization) | ## Causa [#causa] Una clau de prova no pot néixer amb abasts de mòduls vetats a la sandbox. ## Què fer [#què-fer] Treu aquests abasts de la clau de prova i reserva'ls per a la clau de producció que operarà sobre l'empresa real. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autorització](/ca/errors/index-authorization) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # seat_charge_failed (/ca/errors/seat_charge_failed) | Code | Type | HTTP | Categoria | | -------------------- | ------------------------ | ---- | -------------------------------------- | | `seat_charge_failed` | `payment_required_error` | 402 | [Empreses](/ca/errors/index-companies) | ## Causa [#causa] El cobrament immediat del prorrateig del seient va ser rebutjat: la targeta es va denegar, necessita autenticació, o el proveïdor de pagament era inaccessible. L'empresa no es crea si el seient no es cobra. ## Què fer [#què-fer] Arregla el mètode de pagament al portal de facturació i repeteix l'operació; consulta amb el teu banc si la targeta es continua denegant. ## Relacionat [#relacionat] * [Tots els codis d'error d'Empreses](/ca/errors/index-companies) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # send_failed (/ca/errors/send_failed) | Code | Type | HTTP | Categoria | | ------------- | ----------- | ---- | ----------------------------------- | | `send_failed` | `api_error` | 500 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] El document no es va lliurar per correu: el proveïdor de correu va rebutjar el missatge o era inaccessible. ## Què fer [#què-fer] Revisa l'adreça del destinatari i repeteix l'enviament; el document no queda afectat, només el seu lliurament. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_already_archived (/ca/errors/series_already_archived) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | --------------------------------- | | `series_already_archived` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] La sèrie ja estava arxivada, i l'arxivat no es repeteix: una segona crida indica que el client ha perdut l'estat real. ## Què fer [#què-fer] Llegeix la marca `is_archived` de la sèrie abans d'actuar; per recuperar-la fes servir l'operació de desarxivat. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_code_immutable_with_documents (/ca/errors/series_code_immutable_with_documents) | Code | Type | HTTP | Categoria | | -------------------------------------- | ----------------------- | ---- | --------------------------------- | | `series_code_immutable_with_documents` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] Canviar el prefix d'una sèrie que ja va emetre documents reescriuria retroactivament el seu identificador fiscal, mentre els clients i l'AEAT tenen el número original. ## Què fer [#què-fer] Crea una sèrie nova amb el prefix nou i emet des d'ella; l'antiga conserva els documents que ja va numerar. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_has_documents (/ca/errors/series_has_documents) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | --------------------------------- | | `series_has_documents` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] La sèrie ja va numerar documents, així que no es pot eliminar: la seqüència correlativa ha de seguir sent auditable. ## Què fer [#què-fer] Arxiva la sèrie en lloc d'esborrar-la: deixa d'oferir-se en documents nous i conserva el seu històric. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_immutable (/ca/errors/series_immutable) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | --------------------------------- | | `series_immutable` | `invalid_request_error` | 405 | [Series](/ca/errors/index-series) | ## Causa [#causa] Les sèries no són editables ni eliminables via API: la continuïtat legal de la numeració exigeix que el seu prefix, el seu any i el seu comptador es quedin com estan. ## Què fer [#què-fer] Crea una sèrie nova amb els valors que necessitis, i fes servir les operacions d'arxivat i desarxivat per decidir quina està en joc. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_initial_number_creates_gap (/ca/errors/series_initial_number_creates_gap) | Code | Type | HTTP | Categoria | | ----------------------------------- | ----------------------- | ---- | --------------------------------- | | `series_initial_number_creates_gap` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] El número inicial salta més enllà del següent correlatiu natural havent-hi documents de l'any en curs, i aquest buit a la seqüència no és admissible per a l'AEAT. ## Què fer [#què-fer] Fixa el número inicial al següent correlatiu, o obre una sèrie nova si necessites arrencar des d'un altre punt. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_locked_by_verifactu (/ca/errors/series_locked_by_verifactu) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------------- | ---- | --------------------------------- | | `series_locked_by_verifactu` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] Com a mínim una factura de la sèrie té un registre de facturació acceptat per l'AEAT, cosa que congela el prefix, l'any i la base de numeració de la sèrie. ## Què fer [#què-fer] Crea una sèrie nova per al canvi que necessites; en aquesta només segueixen sent editables el nom i la política de reinici del comptador. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_not_found (/ca/errors/series_not_found) | Code | Type | HTTP | Categoria | | ------------------ | ----------------- | ---- | --------------------------------- | | `series_not_found` | `not_found_error` | 404 | [Series](/ca/errors/index-series) | ## Causa [#causa] L'identificador no resol a cap sèrie de numeració de l'empresa autenticada. ## Què fer [#què-fer] Llista les sèries, o localitza'n una pel seu codi indicant el tipus de document si aquest codi es repeteix entre tipus. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_type_invalid (/ca/errors/series_type_invalid) | Code | Type | HTTP | Categoria | | --------------------- | ----------------------- | ---- | --------------------------------- | | `series_type_invalid` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] El tipus de document de la sèrie queda fora del catàleg `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. ## Què fer [#què-fer] Envia un dels valors del catàleg: una sèrie numera exactament un tipus de document. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # series_year_locked (/ca/errors/series_year_locked) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | --------------------------------- | | `series_year_locked` | `invalid_request_error` | 422 | [Series](/ca/errors/index-series) | ## Causa [#causa] La sèrie ja va emetre documents en el seu any vigent. Moure l'any deixaria aquests documents apuntant a un exercici buit mentre la seva base imposable és en un altre. ## Què fer [#què-fer] Arxiva la sèrie de l'any en curs i crea'n una de nova per a l'exercici destí. ## Relacionat [#relacionat] * [Tots els codis d'error de Series](/ca/errors/index-series) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # service_unavailable (/ca/errors/service_unavailable) | Code | Type | HTTP | Categoria | | --------------------- | --------------------------- | ---- | ----------------------------------- | | `service_unavailable` | `service_unavailable_error` | 503 | [Servidor](/ca/errors/index-server) | ## Causa [#causa] El servei, o una dependència que necessita, no pot respondre temporalment. ## Què fer [#què-fer] Reintenta amb retard exponencial; no canviïs el payload, perquè la petició en si és correcta. ## Relacionat [#relacionat] * [Tots els codis d'error de Servidor](/ca/errors/index-server) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # signature_payload_too_large (/ca/errors/signature_payload_too_large) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | ------------------------------------------- | | `signature_payload_too_large` | `invalid_request_error` | 422 | [Albarans](/ca/errors/index-delivery-notes) | ## Causa [#causa] La imatge de la signatura supera la mida admesa per al camp. ## Què fer [#què-fer] Envia la signatura com a PNG només de l'àrea de dibuix, sense reescalar-la cap amunt; una signatura manuscrita cap folgadament dins del límit. ## Relacionat [#relacionat] * [Tots els codis d'error d'Albarans](/ca/errors/index-delivery-notes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # sii_excluded (/ca/errors/sii_excluded) | Code | Type | HTTP | Categoria | | -------------- | ----------------------- | ---- | --------------------------------------- | | `sii_excluded` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'empresa està registrada al SII, i els obligats al SII queden exclosos del reglament VeriFactu. ## Què fer [#què-fer] Continua declarant pel SII; si el registre al SII ja no reflecteix la realitat, corregeix-lo a l'empresa abans d'activar VeriFactu. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # simplified_invoice_cannot_be_substituted (/ca/errors/simplified_invoice_cannot_be_substituted) | Code | Type | HTTP | Categoria | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `simplified_invoice_cannot_be_substituted` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Una de les factures de la llista de substitució no es pot substituir: no és simplificada, està cancel·lada o anul·lada, pertany a una altra empresa, o ja té substitutiva. ## Què fer [#què-fer] Treu aquesta factura de la llista — `error.message` indica el número que bloqueja el lot — i torna a enviar la substitució. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # simplified_invoice_not_allowed (/ca/errors/simplified_invoice_not_allowed) | Code | Type | HTTP | Categoria | | -------------------------------- | ----------------------- | ---- | ------------------------------------- | | `simplified_invoice_not_allowed` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] L'operació no és elegible per a factura simplificada: supera els 3.000 €, o és un lliurament intracomunitari, una exportació, una operació amb inversió del subjecte passiu, o el client necessita factura completa per deduir l'IVA. ## Què fer [#què-fer] Emet una F1 completa identificant el destinatari, o una F3 substitutiva si la simplificada ja es va emetre. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # simplified_limit_exceeded (/ca/errors/simplified_limit_exceeded) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `simplified_limit_exceeded` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] Les línies portarien la factura simplificada (F2) per sobre del límit legal absolut de 3.000 € IVA inclòs. ## Què fer [#què-fer] Abaixa l'import, o emet una factura completa (F1) amb el destinatari plenament identificat. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # sku_already_exists (/ca/errors/sku_already_exists) | Code | Type | HTTP | Categoria | | -------------------- | ---------------- | ---- | -------------------------------------- | | `sku_already_exists` | `conflict_error` | 409 | [Productes](/ca/errors/index-products) | ## Causa [#causa] Un altre producte de l'empresa ja fa servir aquest SKU, i el SKU identifica l'article sense ambigüitat dins del catàleg. ## Què fer [#què-fer] Actualitza el producte existent —busca'l pel SKU— o assigna un altre codi al nou. ## Relacionat [#relacionat] * [Tots els codis d'error de Productes](/ca/errors/index-products) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # stripe_payout_already_reconciled (/ca/errors/stripe_payout_already_reconciled) | Code | Type | HTTP | Categoria | | ---------------------------------- | ----------------------- | ---- | -------------------------------------- | | `stripe_payout_already_reconciled` | `invalid_request_error` | 422 | [Pagaments](/ca/errors/index-payments) | ## Causa [#causa] La liquidació ja estava conciliada, i la conciliació és terminal: repetir-la comptabilitzaria dues vegades l'apunt bancari. ## Què fer [#què-fer] Llegeix la liquidació per veure la conciliació registrada; si és incorrecta, corregeix el moviment bancari amb què es va casar. ## Relacionat [#relacionat] * [Tots els codis d'error de Pagaments](/ca/errors/index-payments) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # stripe_payout_not_found (/ca/errors/stripe_payout_not_found) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------- | ---- | -------------------------------------- | | `stripe_payout_not_found` | `not_found_error` | 404 | [Pagaments](/ca/errors/index-payments) | ## Causa [#causa] L'identificador no resol a cap liquidació de l'empresa autenticada. ## Què fer [#què-fer] Llista les liquidacions per obtenir un `id` vigent; apareixen quan Stripe les reporta, no en el moment del cobrament. ## Relacionat [#relacionat] * [Tots els codis d'error de Pagaments](/ca/errors/index-payments) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # suplido_line_cannot_carry_taxes (/ca/errors/suplido_line_cannot_carry_taxes) | Code | Type | HTTP | Categoria | | --------------------------------- | ----------------------- | ---- | ------------------------------------- | | `suplido_line_cannot_carry_taxes` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La línia de suplert porta càrrega pròpia: tipus d'IVA, retenció, recàrrec d'equivalència, descompte, clau de règim, causa d'exempció o producte/paquet. Un suplert no és una operació de l'emissor, així que repercutir-hi un impost seria tributar per un lliurament que no has fet, i lligar-lo a un producte mouria un estoc que mai no has venut. ## Què fer [#què-fer] Deixa la línia a zero a `tax_rate`, `retention_rate`, `surcharge_rate` i `discount_percent` — `tax_rate: 0` explícit, perquè ometre'l aplica el 21 % per defecte — i treu `product_id`, `pack_id`, `regime_key` i `exemption_reason`; `error.details.offending_field` anomena el camp que hi sobra. Si l'import sí que porta el teu IVA, la línia és `NORMAL`. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # suplido_not_allowed_in_simplified_invoice (/ca/errors/suplido_not_allowed_in_simplified_invoice) | Code | Type | HTTP | Categoria | | ------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `suplido_not_allowed_in_simplified_invoice` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La factura és simplificada (F2) i una simplificada no identifica el destinatari. Sense destinatari identificat no hi ha a qui acreditar el pagament per compte d'altri, així que l'import no admet el tractament de suplert en aquest tipus de factura. ## Què fer [#què-fer] Emet una factura completa (F1) identificant el client per incloure-hi el suplert, o deixa el suplert fora de la simplificada i repercuteix-lo en una factura a part. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # suplido_requires_source_invoice_reference (/ca/errors/suplido_requires_source_invoice_reference) | Code | Type | HTTP | Categoria | | ------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `suplido_requires_source_invoice_reference` | `invalid_request_error` | 422 | [Factures](/ca/errors/index-invoices) | ## Causa [#causa] La línia de suplert no informa `source_invoice_reference`, el número del justificant que el tercer va expedir a nom del client. Sense aquest justificant el pagament no s'acredita com a fet per compte d'altri i Hisenda el tractaria com a base imposable pròpia de l'emissor, amb el seu IVA repercutit. ## Què fer [#què-fer] Afegeix el número de la factura o de la taxa emesa a nom del client. Si el justificant està al teu nom, no és un suplert: factura'l com a línia `NORMAL` amb el seu tipus d'IVA. ## Relacionat [#relacionat] * [Tots els codis d'error de Factures](/ca/errors/index-invoices) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # supplier_has_documents (/ca/errors/supplier_has_documents) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ---------------------------------------- | | `supplier_has_documents` | `invalid_request_error` | 422 | [Proveïdors](/ca/errors/index-suppliers) | ## Causa [#causa] El proveïdor està referenciat per factures de compra registrades, i esborrar-lo deixaria aquestes despeses sense la part que les va emetre. ## Què fer [#què-fer] Desactiva el proveïdor en lloc d'esborrar-lo: deixa d'aparèixer als selectors i les seves factures conserven la referència. ## Relacionat [#relacionat] * [Tots els codis d'error de Proveïdors](/ca/errors/index-suppliers) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # supplier_not_found (/ca/errors/supplier_not_found) | Code | Type | HTTP | Categoria | | -------------------- | ----------------- | ---- | ---------------------------------------- | | `supplier_not_found` | `not_found_error` | 404 | [Proveïdors](/ca/errors/index-suppliers) | ## Causa [#causa] L'identificador no resol a cap proveïdor de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id`, o busca el proveïdor per `tax_id` o per `external_id` abans de crear un duplicat. ## Relacionat [#relacionat] * [Tots els codis d'error de Proveïdors](/ca/errors/index-suppliers) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # system_tax_default_modification_forbidden (/ca/errors/system_tax_default_modification_forbidden) | Code | Type | HTTP | Categoria | | ------------------------------------------- | --------------------- | ---- | ---------------------------------- | | `system_tax_default_modification_forbidden` | `authorization_error` | 403 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] Els defaults dels impostos del catàleg compartit no es fixen sobre l'impost: el catàleg és global i la preferència és de la teva empresa. ## Què fer [#què-fer] Fixa el default per l'endpoint de defaults fiscals de l'empresa (`POST /v1/companies/me/tax-defaults`), no pel de l'impost. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # system_tax_immutable (/ca/errors/system_tax_immutable) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `system_tax_immutable` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'impost pertany al catàleg canònic AEAT que porta el producte. El seu tipus, el seu codi i el seu nom són fixos perquè totes les empreses comparteixin la mateixa referència fiscal. ## Què fer [#què-fer] Crea un impost propi amb els valors que necessitis, o fes servir les operacions que sí admeten els impostos del sistema: activar, desactivar i marcar-los com a default. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # system_tax_immutable_field (/ca/errors/system_tax_immutable_field) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------------- | ---- | ---------------------------------- | | `system_tax_immutable_field` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'actualització toca un camp congelat en un impost del sistema; `error.param` diu quin. ## Què fer [#què-fer] Treu aquest camp del payload: en els impostos del sistema només canvien la marca d'actiu i les de default. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # system_tax_undeletable (/ca/errors/system_tax_undeletable) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ---------------------------------- | | `system_tax_undeletable` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] Els impostos del sistema formen part del catàleg fiscal compartit i no s'eliminen: esborrar-los trencaria els documents que els referencien. ## Què fer [#què-fer] Desactiva l'impost si no vols que se segueixi oferint; en desactivar-lo es netegen a més les seves marques de default. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_applies_to_invalid (/ca/errors/tax_applies_to_invalid) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ---------------------------------- | | `tax_applies_to_invalid` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'àmbit de l'impost queda fora del catàleg `sale`, `purchase`, `both`. ## Què fer [#què-fer] Envia `sale` per a impostos repercutits en vendes, `purchase` per als suportats en compres, o `both` quan s'apliqui als dos costats. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_code_already_exists (/ca/errors/tax_code_already_exists) | Code | Type | HTTP | Categoria | | ------------------------- | ---------------- | ---- | ---------------------------------- | | `tax_code_already_exists` | `conflict_error` | 409 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] Un altre impost del catàleg ja fa servir aquest codi, i el codi identifica l'impost sense ambigüitat. ## Què fer [#què-fer] Reutilitza l'impost existent, o tria un altre codi per al nou. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_id_already_exists (/ca/errors/tax_id_already_exists) | Code | Type | HTTP | Categoria | | ----------------------- | ---------------- | ---- | ----------------------------------- | | `tax_id_already_exists` | `conflict_error` | 409 | [Clients](/ca/errors/index-clients) | ## Causa [#causa] Un altre client de l'empresa ja té aquest NIF, i el NIF identifica la part sense ambigüitat dins d'una empresa. ## Què fer [#què-fer] Reutilitza el client existent —busca'l pel NIF— o corregeix el valor si es va teclejar malament. ## Relacionat [#relacionat] * [Tots els codis d'error de Clients](/ca/errors/index-clients) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_id_required (/ca/errors/tax_id_required) | Code | Type | HTTP | Categoria | | ----------------- | ----------------------- | ---- | ---------------------------------- | | `tax_id_required` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'operació necessita el número d'identificació fiscal (NIF, CIF o NIE) de la part implicada i el registre no en té. ## Què fer [#què-fer] Omple `tax_id` al client, al proveïdor o a l'empresa abans de repetir l'operació. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_in_use (/ca/errors/tax_in_use) | Code | Type | HTTP | Categoria | | ------------ | ----------------------- | ---- | ---------------------------------- | | `tax_in_use` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'impost està referenciat per documents, productes o proveïdors. Eliminar-lo deixaria documents històrics sense la seva referència fiscal. ## Què fer [#què-fer] Desactiva'l en lloc d'esborrar-lo: deixa d'oferir-se en documents nous i els existents conserven la seva referència. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_inactive_cannot_be_default (/ca/errors/tax_inactive_cannot_be_default) | Code | Type | HTTP | Categoria | | -------------------------------- | ----------------------- | ---- | ---------------------------------- | | `tax_inactive_cannot_be_default` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] Un impost desactivat no pot quedar com a default, ni global ni per tipus de document: seria un default ocult que cap formulari pot triar. ## Què fer [#què-fer] Activa abans l'impost i marca'l després com a default. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_not_found (/ca/errors/tax_not_found) | Code | Type | HTTP | Categoria | | --------------- | ----------------- | ---- | ---------------------------------- | | `tax_not_found` | `not_found_error` | 404 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] L'identificador no correspon a cap impost del catàleg accessible per a aquesta empresa. ## Què fer [#què-fer] Llista el catàleg i fes servir l'`id` que retorna; l'impost també pot quedar fora per la zona AEAT de la teva empresa. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_report_not_found (/ca/errors/tax_report_not_found) | Code | Type | HTTP | Categoria | | ---------------------- | ----------------- | ---- | ------------------------------------------------ | | `tax_report_not_found` | `not_found_error` | 404 | [Informes fiscals](/ca/errors/index-tax-reports) | ## Causa [#causa] L'identificador no resol a cap declaració de l'empresa autenticada. ## Què fer [#què-fer] Llista les declaracions per obtenir un `id` vigent, o genera la del període abans de llegir-la. ## Relacionat [#relacionat] * [Tots els codis d'error d'Informes fiscals](/ca/errors/index-tax-reports) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_report_type_invalid (/ca/errors/tax_report_type_invalid) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ------------------------------------------------ | | `tax_report_type_invalid` | `invalid_request_error` | 422 | [Informes fiscals](/ca/errors/index-tax-reports) | ## Causa [#causa] El tipus de declaració queda fora del catàleg `modelo_303`, `modelo_347`, `modelo_130`. ## Què fer [#què-fer] Envia el model que necessites: 303 IVA trimestral, 130 pagament fraccionat de l'IRPF, 347 operacions anuals amb tercers. ## Relacionat [#relacionat] * [Tots els codis d'error d'Informes fiscals](/ca/errors/index-tax-reports) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # tax_type_invalid (/ca/errors/tax_type_invalid) | Code | Type | HTTP | Categoria | | ------------------ | ----------------------- | ---- | ---------------------------------- | | `tax_type_invalid` | `invalid_request_error` | 422 | [Impostos](/ca/errors/index-taxes) | ## Causa [#causa] El tipus d'impost queda fora del catàleg `vat`, `retention`, `surcharge`, `other`. ## Què fer [#què-fer] Envia un dels quatre tipus: decideix el rang de tipus impositiu admès i com participa l'impost en els totals. ## Relacionat [#relacionat] * [Tots els codis d'error d'Impostos](/ca/errors/index-taxes) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # timeout_seconds_out_of_range (/ca/errors/timeout_seconds_out_of_range) | Code | Type | HTTP | Categoria | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `timeout_seconds_out_of_range` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] `timeout_seconds` queda fora del rang d'1 a 30 segons. ## Què fer [#què-fer] Envia un valor dins del rang; si el teu receptor necessita més, confirma l'esdeveniment immediatament i processa'l de manera asíncrona al teu costat. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # too_many_auth_failures (/ca/errors/too_many_auth_failures) | Code | Type | HTTP | Categoria | | ------------------------ | ---------------------- | ---- | ----------------------------------------------- | | `too_many_auth_failures` | `authentication_error` | 429 | [Autenticació](/ca/errors/index-authentication) | ## Causa [#causa] Van arribar massa intents fallits d'autenticació des de la mateixa adreça, així que queda bloquejada temporalment per frenar els intents d'endevinar credencials. ## Què fer [#què-fer] Atura els reintents, corregeix la clau i espera cinc minuts abans de tornar-ho a provar. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Massa intents fallits d'autenticació des d'aquesta IP. Espera 5 minuts abans de tornar-ho a provar. ## Relacionat [#relacionat] * [Tots els codis d'error d'Autenticació](/ca/errors/index-authentication) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # too_many_custom_headers (/ca/errors/too_many_custom_headers) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ------------------------------------- | | `too_many_custom_headers` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] L'endpoint declara més de 20 capçaleres personalitzades. ## Què fer [#què-fer] Deixa només les capçaleres que el teu receptor necessita de debò; l'autenticació sol cabre en una. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # unknown_filter (/ca/errors/unknown_filter) | Code | Type | HTTP | Categoria | | ---------------- | ----------------------- | ---- | ----------------------------------- | | `unknown_filter` | `invalid_request_error` | 422 | [Request](/ca/errors/index-request) | ## Causa [#causa] Un llistat va rebre un filtre que no coneix. Els parsers canònics de v1 reporten això com a `parameter_unknown`; aquest codi sobreviu per als endpoints encara sense migrar. ## Què fer [#què-fer] Treu el filtre, o substitueix-lo per un dels camps que l'endpoint documenta com a filtrables. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # unsupported_api_version (/ca/errors/unsupported_api_version) | Code | Type | HTTP | Categoria | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `unsupported_api_version` | `invalid_request_error` | 400 | [Request](/ca/errors/index-request) | ## Causa [#causa] La capçalera `Factuarea-Version` està ben formada però anomena una versió fora del conjunt suportat. ## Què fer [#què-fer] Envia una de les dates de versió suportades, o omet la capçalera per fer servir la versió fixada a la teva clau API. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # unsupported_format (/ca/errors/unsupported_format) | Code | Type | HTTP | Categoria | | -------------------- | ----------------------- | ---- | ------------------------------------------------ | | `unsupported_format` | `invalid_request_error` | 422 | [Informes fiscals](/ca/errors/index-tax-reports) | ## Causa [#causa] El format demanat no està disponible per a aquest model: no tota declaració produeix totes les sortides. ## Què fer [#què-fer] Demana un dels formats que sí que ofereix el model: el fitxer de text per a l'AEAT, el PDF o el full de càlcul. ## Relacionat [#relacionat] * [Tots els codis d'error d'Informes fiscals](/ca/errors/index-tax-reports) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # unsupported_media_type (/ca/errors/unsupported_media_type) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `unsupported_media_type` | `invalid_request_error` | 415 | [Request](/ca/errors/index-request) | ## Causa [#causa] Una petició amb body va declarar un `Content-Type` diferent de `application/json`. ## Què fer [#què-fer] Fixa `Content-Type: application/json` i serialitza el body com a JSON. ## Missatge que retorna l'API [#missatge-que-retorna-lapi] > Només s'accepta Content-Type application/json. ## Relacionat [#relacionat] * [Tots els codis d'error de Request](/ca/errors/index-request) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # verifactu_already_submitted (/ca/errors/verifactu_already_submitted) | Code | Type | HTTP | Categoria | | ----------------------------- | ----------------------- | ---- | --------------------------------------- | | `verifactu_already_submitted` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] La factura ja té el seu registre d'alta. Existeix exactament una alta per factura, així que una segona trencaria la idempotència de la cadena. ## Què fer [#què-fer] Llegeix el registre existent en lloc de crear-ne un altre; per canviar el que s'ha declarat, emet una rectificativa. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # verifactu_mode_invalid (/ca/errors/verifactu_mode_invalid) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `verifactu_mode_invalid` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] El mode queda fora del catàleg `verifactu` / `no_verifactu`. ## Què fer [#què-fer] Envia `verifactu` per declarar a l'AEAT en temps real, o `no_verifactu` per al mode de registre local. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # verifactu_not_eligible (/ca/errors/verifactu_not_eligible) | Code | Type | HTTP | Categoria | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `verifactu_not_eligible` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] La factura no es pot registrar ara mateix a l'AEAT: l'empresa no està en mode VeriFactu, no té certificat actiu, o el certificat està revocat o emès per a un altre NIF. ## Què fer [#què-fer] Activa el mode VeriFactu i puja un certificat FNMT vàlid amb el NIF de l'empresa; els registres diferits per aquest motiu es reencuen tan bon punt hi ha certificat vàlid. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # verifactu_record_not_found (/ca/errors/verifactu_record_not_found) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------- | ---- | --------------------------------------- | | `verifactu_record_not_found` | `not_found_error` | 404 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'identificador no correspon a cap registre de facturació de l'empresa autenticada. ## Què fer [#què-fer] Revisa l'`id`, o localitza el registre pel seu CSV, per la seva empremta o pel número de la factura que el va generar. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # verifactu_transmission_failed (/ca/errors/verifactu_transmission_failed) | Code | Type | HTTP | Categoria | | ------------------------------- | ----------------------- | ---- | --------------------------------------- | | `verifactu_transmission_failed` | `invalid_request_error` | 422 | [VeriFactu](/ca/errors/index-verifactu) | ## Causa [#causa] L'enviament del registre a l'AEAT no es va completar: l'endpoint era inaccessible o va respondre amb una incidència. ## Què fer [#què-fer] Consulta l'estat del registre — la transmissió es reintenta sola amb retard exponencial — i força un reintent quan hagi passat la finestra d'espera. ## Relacionat [#relacionat] * [Tots els codis d'error de VeriFactu](/ca/errors/index-verifactu) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # webhook_delivery_not_found (/ca/errors/webhook_delivery_not_found) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------- | ---- | ------------------------------------- | | `webhook_delivery_not_found` | `not_found_error` | 404 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] L'identificador no correspon a cap intent de lliurament, o el lliurament queda fora de la finestra de retenció de l'històric. ## Què fer [#què-fer] Llista els lliuraments de l'endpoint per obtenir un `id` vigent; els lliuraments anteriors a la finestra de retenció ja no estan disponibles. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # webhook_endpoint_degraded (/ca/errors/webhook_endpoint_degraded) | Code | Type | HTTP | Categoria | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `webhook_endpoint_degraded` | `invalid_request_error` | 422 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] L'endpoint està degradat després de fallades repetides de lliurament, així que els pings de prova es rebutgen mentre segueixi en aquest estat. ## Què fer [#què-fer] Arregla el receptor, reactiva l'endpoint i envia després el ping de prova. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # webhook_endpoint_not_found (/ca/errors/webhook_endpoint_not_found) | Code | Type | HTTP | Categoria | | ---------------------------- | ----------------- | ---- | ------------------------------------- | | `webhook_endpoint_not_found` | `not_found_error` | 404 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] L'identificador no resol a cap endpoint de webhook de l'empresa autenticada. ## Què fer [#què-fer] Llista els teus endpoints i fes servir l'`id` que retornen. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # webhook_secret_recently_rotated (/ca/errors/webhook_secret_recently_rotated) | Code | Type | HTTP | Categoria | | --------------------------------- | ------------------ | ---- | ------------------------------------- | | `webhook_secret_recently_rotated` | `rate_limit_error` | 429 | [Webhooks](/ca/errors/index-webhooks) | ## Causa [#causa] El secret de signatura es va rotar fa menys de cinc minuts. La finestra de gràcia permet que el teu receptor accepti tots dos secrets durant el canvi; rotar un altre cop dins d'ella invalidaria signatures encara en vol. ## Què fer [#què-fer] Espera cinc minuts des de l'última rotació, i desplega el secret nou al teu receptor abans de tornar a rotar. ## Relacionat [#relacionat] * [Tots els codis d'error de Webhooks](/ca/errors/index-webhooks) * [Codis d'error per categoria](/ca/errors) * [Taula de referència completa](/ca/guides/errors/all) * [Model d'errors](/ca/guides/errors) --- # FAQ (/ca/faq) Respostes breus a les preguntes que més sorgeixen en desenvolupar contra l'API pública de Factuarea. Cadascuna enllaça amb la guia que la cobreix per complet. ## Accés i claus [#accés-i-claus] ### Com aconsegueixo accés a l'API? [#com-aconsegueixo-accés-a-lapi] L'API està **inclosa en tots els plans de Factuarea** — sense add-on a banda ni sol·licitud d'accés. Crea la teva API key des de [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) i comença a cridar `/v1`. Durant el trial de 10 dies ja tens accés amb el tier `free`; els plans de pagament pugen el tier de rate limit. Consulta [Límits de peticions](/guides/rate-limits). ### Aquesta clau és live o test? [#aquesta-clau-és-live-o-test] Llegeix el **prefix**: `fact_live_` opera sobre la teva empresa real (producció), `fact_test_` sobre un sandbox aïllat. El prefix és la única font de veritat — cap paràmetre de la petició canvia l'entorn. Consulta [Mode de prova i sandbox](/guides/test-mode). ### He perdut el secret de la meva API key. El puc recuperar? [#he-perdut-el-secret-de-la-meva-api-key-el-puc-recuperar] No. El backend només emmagatzema un hash bcrypt del secret, que es mostra **una sola vegada** en crear-lo. Rota la clau des del dashboard per emetre un nou secret i torna a desplegar-lo. Consulta [Autenticació › Rotació](/guides/authentication). ### Com roto una clau sense downtime? [#com-roto-una-clau-sense-downtime] Rota des del dashboard: el secret antic i el nou es mantenen vàlids durant un **període de gràcia**, així que pots desplegar el nou sense perdre peticions. Revoca una clau només quan sospitis una fuita — això la invalida a l'instant (`401 api_key_revoked`). Consulta [Autenticació › Rotació i revocació](/guides/authentication). ## Mode de prova [#mode-de-prova] ### Les dades de test estan aïllades de producció? [#les-dades-de-test-estan-aïllades-de-producció] Sí — de manera estructural, no per un filtre. Una clau `fact_test_` opera sobre una **empresa sandbox** dedicada, així que els recursos creats en test mai són visibles per a una clau `fact_live_` (i viceversa), i la numeració fiscal de test mai toca les teves sèries de producció. Consulta [Mode de prova › Aïllament de dades](/guides/test-mode). ### Per què no es disparen els meus webhooks en mode de prova? [#per-què-no-es-disparen-els-meus-webhooks-en-mode-de-prova] En test, els efectes externs estan desactivats: VeriFactu → AEAT, FACe, els emails i l'**entrega de webhooks** estan tots neutralitzats. Els esdeveniments es continuen registrant amb `livemode: false` i es poden consultar via `GET /v1/events`, però no s'entreguen als teus endpoints. Usa `POST /v1/webhook_endpoints/{id}/ping` per provar el teu receptor. Consulta [Mode de prova › Què està desactivat](/guides/test-mode). ## Documents [#documents] ### Quina diferència hi ha entre delete, annul i void d'una factura? [#quina-diferència-hi-ha-entre-delete-annul-i-void-duna-factura] `DELETE /v1/invoices/{id}` només funciona amb **esborranys**. Un cop emesa una factura no es pot eliminar: usa `POST /v1/invoices/{id}/annul` (registra una raó documentada i crea el registre d'*anul·lació* d'AEAT quan VeriFactu està activat) o `POST /v1/invoices/{id}/void` (irreversible, registra un `void_reason`, rebutjat si la factura ja ha estat rectificada). Consulta [Migració des de Holded › Diferències intencionades](/guides/migration-from-holded). ## Diners i dates [#diners-i-dates] ### Com es representen els imports monetaris? [#com-es-representen-els-imports-monetaris] En **euros**, amb dos decimals, a l'estil Stripe — la forma canònica és una cadena decimal com `"1234.56"`. Parseja els diners com a decimal fix, mai com a float binari, i deixa que l'API calculi els totals a partir de les línies en brut. Consulta [Imports i dates › Diners](/guides/amounts-and-dates). ### Quin format usen les dates? [#quin-format-usen-les-dates] Les dates de calendari com `issued_on` i `due_on` usen `YYYY-MM-DD` (per exemple `2026-05-15`). Les marques de temps com `created` i `expires_at` són **ISO 8601 en UTC** amb sufix `Z` — p. ex. `2026-05-15T10:23:18Z`. Consulta [Imports i dates](/guides/amounts-and-dates). ### Quina zona horària usen les quotes? [#quina-zona-horària-usen-les-quotes] La **quota mensual del límit de peticions** es reinicia el dia 1 a les 00:00 **Europe/Madrid**, mentre que les marques de temps es retornen en UTC. Consulta [Imports i dates › Zona horària de les quotes](/guides/amounts-and-dates) i [Límits de peticions](/guides/rate-limits). ## Idempotència i reintents [#idempotència-i-reintents] ### Què passa si reenvio una Idempotency-Key? [#què-passa-si-reenvio-una-idempotency-key] Dins del TTL de 24h, l'API retorna la resposta **cacheada** (estat, headers i body) sense tornar a executar el handler, afegint el header `Idempotent-Replayed: true`. Un `4xx` cacheat també es reenvia. Reutilitzar la clau amb un body **diferent** respon `409 idempotency_key_reused`. Consulta [Idempotència](/guides/idempotency). ### Un replay idempotent compta contra el meu rate limit? [#un-replay-idempotent-compta-contra-el-meu-rate-limit] No. Una clau reenviada dins del seu TTL **no compta** contra la teva quota. Claus diferents amb el mateix payload compten cadascuna, una a una. Consulta [Idempotència › Què NO és la idempotència](/guides/idempotency). ### Quina és la longitud màxima de la Idempotency-Key? [#quina-és-la-longitud-màxima-de-la-idempotency-key] Entre **1 i 64 caràcters**. Qualsevol valor únic opac serveix (es recomana UUID v7, però UUID v4, ULID o nanoid també valen). Consulta [Idempotència › Format de la clau](/guides/idempotency). ## Límits de peticions i errors [#límits-de-peticions-i-errors] ### Com conec la meva quota restant? [#com-conec-la-meva-quota-restant] Cada resposta (inclosa `429`) porta `X-RateLimit-Limit`, `X-RateLimit-Remaining` i `X-RateLimit-Reset`; un `429` afegeix `Retry-After` amb els segons que cal esperar. Els límits depenen del tier de la teva clau. Consulta [Límits de peticions](/guides/rate-limits). ### Com he de reintentar una petició fallida? [#com-he-de-reintentar-una-petició-fallida] `4xx` (excepte `429`) → no reintentis, corregeix la petició. `429` → respecta `Retry-After`. `5xx` → back-off exponencial amb jitter, fins a 5 intents. Consulta [Errors › Estratègia de reintents](/guides/errors). ### On reporto un problema amb una petició concreta? [#on-reporto-un-problema-amb-una-petició-concreta] Agafa el `request_id` de l'embolcall d'error (també és al header `X-Request-Id`) i envia'l a suport — ens permet correlacionar logs, mètriques i traces. Consulta [Suport](/support). --- # Absències (/ca/guides/absences) El domini d'**absències** té dues capes: una capa de **configuració** (què es pot sol·licitar i quant) i una capa de **flux** (sol·licituds, saldos i calendari). Tot està acotat per `absences:read` / `absences:write` i gatejat pel mòdul `control_horario`, sota `https://api.factuarea.com/v1`. ## Tipus d'absència [#types] Un **tipus d'absència** és el que un empleat pot sol·licitar — vacances, baixa per malaltia, un dia d'assumptes propis. Cada tipus porta: si és **retribuït** (`is_paid`), si **requereix aprovació** (`requires_approval`), una **unitat de mesura** (`days` o `hours`), un **color** hex, una **visibilitat** (`everyone` o `managers_only`) i un estat (`active` / `archived`). El nom és únic per empresa. A cada empresa nova es **sembra** un conjunt de tipus espanyols per defecte, així que sovint arrenques amb un catàleg funcional. | Operació | Endpoint | | -------------------- | ---------------------------------------------------------- | | Llistar / detall | `GET /v1/absence-types`, `GET /v1/absence-types/{type}` | | Crear / actualitzar | `POST /v1/absence-types`, `PATCH /v1/absence-types/{type}` | | Arxivar / desarxivar | `POST /v1/absence-types/{type}/archive`, `.../unarchive` | ```bash curl -X POST https://api.factuarea.com/v1/absence-types \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Assumptes propis", "is_paid": true, "requires_approval": true, "measurement_unit": "days", "color": "#4F46E5", "visibility": "everyone" }' ``` ## Polítiques d'absència [#policies] Una **política d'absència** decideix **quant** i **per a qui**. Fixa una **assignació de dies** — `limited` (un nombre positiu de dies) o `unlimited` —, un **mètode de meritació** (`annual` o `monthly`), el conjunt de **tipus** que cobreix i els **empleats** als quals s'assigna. Associar tipus és un reemplaçament total; una política s'assigna i es desassigna d'empleats en lot. | Operació | Endpoint | | ------------------------------- | ------------------------------------------------------------------ | | Llistar / detall | `GET /v1/absence-policies`, `GET /v1/absence-policies/{policy}` | | Crear / actualitzar | `POST /v1/absence-policies`, `PATCH /v1/absence-policies/{policy}` | | Assignar / desassignar empleats | `POST /v1/absence-policies/{policy}/assign`, `.../unassign` | | Llistar assignacions | `GET /v1/absence-policies/{policy}/assignments` | | Arrossegament | `GET /v1/absence-policies/{policy}/carryover` | | Arxivar / desarxivar | `POST /v1/absence-policies/{policy}/archive`, `.../unarchive` | L'**arrossegament** exposa quanta assignació no consumida passa al següent període de meritació per empleat. L'assignació sempre resol l'empleat dins de l'empresa autenticada, així que una política de l'empresa A mai s'assigna a un empleat de l'empresa B. ```bash curl -X POST https://api.factuarea.com/v1/absence-policies \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Estàndard 22 dies", "allowance": { "type": "limited", "days": 22 }, "accrual_method": "annual", "absence_type_ids": ["01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b"] }' ``` ## Sol·licituds, saldos i calendari [#requests] Quan existeixen tipus i polítiques, els empleats **sol·liciten** absències i els managers les resolen. | Operació | Endpoint | Scope | | --------------------- | ----------------------------------------------------------------- | ---------------- | | Crear una sol·licitud | `POST /v1/absence-requests` | `absences:write` | | Aprovar / rebutjar | `POST /v1/absence-requests/{request}/approve`, `.../reject` | `absences:write` | | Cancel·lar | `POST /v1/absence-requests/{request}/cancel` | `absences:write` | | Llistar / detall | `GET /v1/absence-requests`, `GET /v1/absence-requests/{request}` | `absences:read` | | Saldos | `GET /v1/absence-balances`, `GET /v1/absence-balances/{employee}` | `absences:read` | | Calendari d'equip | `GET /v1/absence-calendar` | `absences:read` | Un **saldo** és l'assignació restant per empleat i tipus, derivada de la meritació de la política menys les sol·licituds aprovades. El **calendari** retorna les absències de l'equip en un rang de dates — la vista del manager de qui és fora i quan. ```bash curl -X POST https://api.factuarea.com/v1/absence-requests \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "absence_type_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "start_date": "2026-08-01", "end_date": "2026-08-15" }' ``` Un tipus amb `requires_approval: false` es concedeix en sol·licitar-lo; un amb `requires_approval: true` espera que un manager l'aprovi o el rebutgi abans de descomptar-lo del saldo. ## Flux típic [#flow] 1. Revisa els **tipus** sembrats, o crea els teus. 2. Crea **polítiques** amb una assignació de dies i una meritació, i cobreix els tipus pertinents. 3. **Assigna** cada política als seus empleats. 4. Els empleats **sol·liciten**; els managers **aproven** o **rebutgen**. 5. Llegeix **saldos** i el **calendari**, i consulta l'**arrossegament** al tancament de l'any. Els festius que afecten les absències viuen al seu propi domini de només lectura — consulta la [visió general](/guides/workforce-overview) i la [referència de festius](/api-reference/holidays/public-api.v1.holidays.list). ## Pròxims passos [#next] * [Tancament mensual](/guides/monthly-time-close) — les absències aprovades alimenten l'informe mensual. * Navega la referència de [tipus](/api-reference/absence-types/public-api.v1.absence-types.list), [polítiques](/api-reference/absence-policies/public-api.v1.absence-policies.list) i [sol·licituds](/api-reference/absence-requests/public-api.v1.absence-requests.create). --- # Personalització del compte (/ca/guides/account-personalization) La personalització controla **l'aspecte i la lectura de les teves factures**: l'idioma en què es genera el PDF, la plantilla PDF que l'emmarca i el color d'accent que l'identifica. Tots tres viuen a l'empresa autenticada i s'apliquen a tots els documents que l'API genera per a tu. Llegeixes els valors actuals des del bloc `personalization` de `GET /v1/account`, i els canvies amb una única actualització parcial a `PATCH /v1/account/personalization`. Tots dos endpoints funcionen igual en mode de prova (claus `fact_test_`) i en producció (claus `fact_live_`). ## Els tres ajustos [#els-tres-ajustos] | Ajust | Camp | Valors acceptats | | ---------------------------- | -------------- | -------------------------------------------------------- | | Idioma d'emissió de factures | `language` | `es`, `en`, `ca` | | Plantilla PDF | `pdf_template` | `classic`, `modern`, `minimal`, `corporative`, `premium` | | Color d'accent | `accent_color` | hexadecimal `#RRGGBB`, o `null` per netejar-lo | ### Idioma d'emissió de factures [#idioma-demissió-de-factures] `language` és el locale en què es genera el **PDF**. Posa'l a `en` i els títols, etiquetes i dates de cada PDF que generis passen a anglès; `ca` els mostra en català; `es` (per defecte) en castellà. No canvia el text `message` dels errors de l'API — aquests continuen en castellà, tal com documenta el [model d'errors](/guides/errors). ### Plantilla PDF [#plantilla-pdf] `pdf_template` és un slug del catàleg tancat `PdfTemplate`. Les cinc plantilles de sistema són `classic`, `modern` (per defecte), `minimal`, `corporative` i `premium`. Quines pot seleccionar el teu compte depèn del teu pla — descobreix el conjunt permès amb [l'endpoint de plantilles](#descobrir-les-plantilles-disponibles) en lloc de fixar-lo a mà. ### Color d'accent [#color-daccent] `accent_color` és el color hexadecimal `#RRGGBB` amb què s'identifica el PDF (capçaleres, totals, accents). Envia `null` per netejar-lo i tornar al valor per defecte de la plantilla. ## Llegir la personalització actual [#llegir-la-personalització-actual] El bloc `personalization` forma part del recurs `Account`: ```bash curl -s https://api.factuarea.com/v1/account \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ | jq '.data.personalization' ``` ```json { "language": "es", "pdf_template": "modern", "accent_color": "#1a73e8" } ``` `language` i `pdf_template` sempre hi són presents. `accent_color` és `null` quan no hi ha cap color configurat. ## Actualitzar la personalització [#actualitzar-la-personalització] `PATCH /v1/account/personalization` és una **actualització parcial**: només s'apliquen els camps que envies, i qualsevol camp que ometis manté el seu valor actual. La resposta és el **recurs `Account` actualitzat** — amb la mateixa forma que `GET /v1/account`, inclòs el bloc `personalization` tot just refrescat. Requereix el scope `account:write`. ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "en", "pdf_template": "premium", "accent_color": "#0F766E" }' \ | jq '.data.personalization' ``` Canvia un únic ajust enviant només aquest camp: ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "ca" }' ``` Neteja el color d'accent enviant `null`: ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "accent_color": null }' ``` Cada ajust és independent: `language`, `pdf_template` i `accent_color` no es trepitgen entre si. Enviar-ne un mai reinicia els altres dos. ### Errors de validació [#errors-de-validació] Cada ajust es valida contra el seu catàleg tancat. Un valor fora del catàleg retorna `422` amb els `allowed_values` del camp erroni — `language` i `pdf_template` contra el seu enum, `accent_color` contra el patró `#RRGGBB`: ```json { "error": { "type": "validation_error", "code": "validation_failed", "message": "El idioma indicado no es válido.", "param": "language", "allowed_values": ["es", "en", "ca"] } } ``` ## Descobrir les plantilles disponibles [#descobrir-les-plantilles-disponibles] `GET /v1/account/personalization/templates` llista les plantilles PDF disponibles per al pla del teu compte (segons el pla) juntament amb el format acceptat per a `accent_color`. Fes-lo servir per omplir un selector en lloc de fixar el catàleg a mà. Requereix el scope `account:read`. ```bash curl -s https://api.factuarea.com/v1/account/personalization/templates \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ | jq '.data' ``` ```json { "object": "personalization_templates", "templates": [ { "slug": "classic", "label": "Clásica", "available": true }, { "slug": "modern", "label": "Moderna", "available": true }, { "slug": "minimal", "label": "Minimalista", "available": true }, { "slug": "corporative", "label": "Corporativa", "available": false }, { "slug": "premium", "label": "Premium", "available": false } ], "accent_color": { "format": "#RRGGBB", "example": "#1a73e8" } } ``` L'indicador `available` reflecteix el teu **pla actual**: un slug en `false` existeix al catàleg però no es pot fixar fins que milloris de pla. Ofereix només les plantilles disponibles, i llegeix `accent_color.format` per validar el color al client abans del `PATCH`. ## Scopes [#scopes] | Operació | Endpoint | Scope | | ------------------------------ | ------------------------------------------- | --------------- | | Llegir la personalització | `GET /v1/account` | `account:read` | | Llistar plantilles | `GET /v1/account/personalization/templates` | `account:read` | | Actualitzar la personalització | `PATCH /v1/account/personalization` | `account:write` | --- # Actuar en nom d'una filla (/ca/guides/acting-on-behalf) Un cop tens [empreses gestionades](/guides/companies) sota el teu tenant mestre, hi ha dues maneres d'actuar sobre una d'elles. Pots emetre una [API key filla](/guides/child-api-keys) lligada a ella — útil quan vols una credencial acotada a una sola empresa. O pots seguir fent servir la teva **master key** i triar l'empresa objectiu per petició amb el header `X-Active-Profile`. Així, una sola master key opera sobre qualsevol empresa del teu arbre, sense reautenticar-te ni gestionar una key per NIF. Aquesta pàgina cobreix el header. Posa al header l'`id` públic (UUID v7) de l'empresa filla sobre la qual vols actuar. Funciona sobre **qualsevol** endpoint — factures, clients, sèries i la resta: ```bash curl https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "X-Active-Profile: 01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c" ``` Quan el header és present i l'empresa és teva, **tota la petició** s'executa sobre les dades d'aquesta empresa filla: cada lectura es filtra a ella i cada escriptura hi cau. La petició es resol al `company_id` de l'empresa filla abans del rate limit i la idempotència, de manera que cada empresa té els seus propis buckets. El header és **opcional i additiu**. Omet-lo i la petició opera sobre l'empresa a la qual pertany la teva key — exactament com abans. Les integracions existents continuen funcionant sense canvis. El header **mai** amplia la teva key. Els seus `scopes`, `tier` i `environment` es conserven intactes: una master key amb només `invoices:read` que apunta a una empresa filla segueix sense poder fer `POST /v1/invoices` allà (`403 insufficient_scope`), i una key `fact_test_` es manté al sandbox sigui quin sigui el perfil actiu. Canviar de perfil canvia **sobre quina** empresa actues, mai **què** tens permès fer. L'empresa filla hereta el pla i els add-ons del mestre. ## Resolució i errors [#resolution] `X-Active-Profile` resol l'empresa activa abans que s'executi cap handler: | Header | Resultat | | ---------------------------------------------------------------- | ------------------------------------------------------------------ | | Absent o buit | La petició opera sobre l'empresa a la qual pertany la key (no-op). | | L'`id` de la teva pròpia empresa mestra | Permès — equival a ometre el header. | | Una empresa filla que posseeixes, `active` | La petició opera sobre aquesta empresa filla. | | Una empresa filla que posseeixes, però `inactive` | `403 company_inactive` — reactiva-la primer. | | No és un UUID v7 vàlid | `400 parameter_invalid_uuid`, amb `param: "X-Active-Profile"`. | | Una empresa que **no** posseeixes (un altre arbre, o inexistent) | `404 profile_not_found`. | El `404` és **indistingible** tant si l'empresa pertany a un altre mestre com si no existeix — l'API mai revela que una empresa fora del teu arbre existeix: ```json { "error": { "type": "not_found_error", "code": "profile_not_found", "message": "El perfil de empresa indicado no existe o no pertenece a tu cuenta.", "param": "X-Active-Profile" } } ``` El `403` és diferent: l'empresa **sí** és teva, així que revelar que està desactivada és legítim — és el senyal per [reactivar-la](/guides/companies#activate) abans d'operar: ```json { "error": { "type": "authorization_error", "code": "company_inactive", "message": "Esta empresa está desactivada. Actívala para operar.", "param": "X-Active-Profile" } } ``` ## Quina hauries de fer servir? [#which] `X-Active-Profile` i les [API keys filles](/guides/child-api-keys) resolen necessitats diferents i coexisteixen: * Fes servir una **key filla** per lliurar una credencial acotada a una integració lligada a una empresa — la credencial en si queda lligada a aquesta empresa. * Fes servir el **header** per gestionar moltes empreses des d'una sola master key — una credencial, empresa objectiu triada per petició. El header només canvia l'empresa activa. Mai canvia els scopes de la key, i l'aïllament entre mestres s'aplica igual que en [gestionar les empreses](/guides/companies#scopes): una empresa fora del teu arbre mai és observable, retornant `404` en comptes de `403`. --- # Imports i dates (/ca/guides/amounts-and-dates) Cada valor monetari, de data i d'hora de l'API pública segueix un petit conjunt de convencions fixes. Són les mateixes a tots els recursos, així que tan aviat com les gestiones en un lloc el teu client funciona a tot arreu. ## Diners [#diners] Els imports sempre van en **euros (EUR)** — el camp `currency` és present a tots els documents i és `"EUR"` a v1 ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)). Encara no hi ha suport multidivisa. Els imports porten **dos decimals** (precisió de cèntims). La representació canònica és un **string decimal** amb exactament dos decimals, a l'estil Stripe: ```json { "price": "1234.56" } ``` Alguns recursos emeten actualment els imports com a **números** JSON (floats) en lloc de strings decimals — per exemple el `total`, el `subtotal` o el `unit_price` d'un document tornen com a `1802.9`, `968`, `100`. Escriu el teu parser perquè accepti **tant** un string com un número en qualsevol camp de diners, i normalitza'l a un tipus decimal fix al teu costat (p. ex. `Decimal` a Python, un big-decimal o un enter d'unitats menors a JS). No guardis mai els diners com un float binari en cru. ### Deixa que l'API calculi els totals [#deixa-que-lapi-calculi-els-totals] **No arrodoneixis ni calculis per endavant.** Envia les dades en cru de cada línia (`quantity`, `unit_price`, `discount`, el `*_id` de l'impost) i deixa que l'API derivi el subtotal, l'IVA, el recàrrec, la retenció i el total general. El servidor és l'única font de veritat per a cada total — si arrodoneixes els imports de línia pel teu compte abans d'enviar-los, les teves xifres poden divergir del que emmagatzema l'API. El total d'un document segueix una sola fórmula a tota l'API: ``` total = subtotal + total_vat + total_surcharge − total_retention ``` Si necessites previsualitzar el desglossament **abans** de crear un document — per a un resum de comanda, un carretó o per conciliar les teves pròpies xifres — crida `POST /v1/taxes/calculate-totals` amb les línies i llegeix el `subtotal`, el `total_vat`, el `total_surcharge`, el `total_retention` i el `total` calculats (imports en EUR), més un desglossament per línia en el mateix ordre: ```json { "subtotal": 250, "total_vat": 52.5, "total_surcharge": 0, "total_retention": 15, "total": 287.5, "lines": [ { "subtotal": 100, "vat_amount": 21, "surcharge_amount": 0, "retention_amount": 0, "total": 121 } ] } ``` El mateix desglossament d'impostos per línia aplica a tots els documents de venda: **factures, pressupostos, proformes i albarans** accepten tots un `retention_rate` i un `surcharge_rate` per línia (retenció d'IRPF i recàrrec d'equivalència, 0–100), i la seva capçalera porta els `total_vat`, `total_surcharge` i `total_retention` agregats. La mateixa fórmula es compleix a tot arreu. Cada línia a més retorna `retention_rate` i `surcharge_rate` a la resposta, perquè puguis reconciliar el desglossament línia a línia. **El recàrrec d'equivalència segueix parells legals fixos.** Quan una línia declara un `surcharge_rate`, ha de coincidir amb el tipus d'IVA d'aquesta línia segons el règim espanyol: **21% → 5.2%**, **10% → 1.4%**, **4% → 0.5%**, **0% → 0%**. Un parell il·legal (p. ex. `tax_rate: 21` amb `surcharge_rate: 1.4`) es rebutja amb `422` i els parells permesos es retornen a `error.allowed_values`. Envia només el recàrrec que admet el tipus d'IVA de la línia. ## Factures de compra — impost per línia [#factures-de-compra--impost-per-línia] Una factura de compra registra el que et va cobrar un **proveïdor**, així que cadascuna de les seves línies accepta uns qualificadors fiscals extra que el costat de venda cobreix a la seva manera. A `CreatePurchaseInvoiceRequest.lines[]` pots fixar: | Camp | Tipus | Significat | | ------------------ | -------------- | ---------------------------------------------------------------------- | | `retention_rate` | number (0–100) | Retenció d'IRPF aplicada a la línia. | | `surcharge_rate` | number | Recàrrec d'equivalència, mateixos parells legals que en venda. | | `vat_deductible` | boolean | Informatiu — marca l'IVA com a deduïble. **No** canvia l'import pagat. | | `exemption_reason` | enum o `null` | Per què la línia està exempta o no subjecta (vegeu sota). | El total per línia segueix la mateixa forma que al costat de venda, restant la retenció i sumant el recàrrec: ``` total de línia = subtotal + impostos − retention_amount + surcharge_amount ``` El `surcharge_rate` d'una línia de compra segueix els **mateixos parells legals IVA↔recàrrec** que una línia de venda, validats al servidor: **21 → 5.2**, **10 → 1.4**, **4 → 0.5**, **0 → 0**. El parell `0 → 0` també és vàlid al costat de compra — una línia exempta o a tipus zero porta un recàrrec zero. Un parell il·legal es rebutja amb `422`. `exemption_reason` qualifica per què una línia queda fora de l'IVA ordinari. És un enum o `null` (o absent), cosa que significa que la línia hereta la qualificació de la capçalera de la factura, o no en declara cap: | Valor | Classe | | ---------------------------------- | --------------------------------------- | | `E1`, `E2`, `E3`, `E4`, `E5`, `E6` | Exempta (causa d'exempció de la LIVA) | | `N1`, `N2` | No subjecta (causa de no subjecció) | | `null` | Heretar de la capçalera / cap declarada | Una línia de factura de compra que porta alhora un recàrrec i un motiu d'exempció: ```json { "description": "Wholesale goods", "quantity": 10, "unit_price": "50.00", "tax_rate": 21, "surcharge_rate": 5.2, "retention_rate": 0, "vat_deductible": true, "exemption_reason": null } ``` ```bash curl -s -X POST https://api.factuarea.com/v1/purchase_invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "supplier_id": "01931b3e-...s01", "received_on": "2026-03-15", "lines": [ { "description": "Exempt service", "quantity": 1, "unit_price": "200.00", "tax_rate": 0, "surcharge_rate": 0, "exemption_reason": "E1" } ] }' | jq '.data | {subtotal, total_vat, total_surcharge, total_retention, total}' ``` **Cèntims als informes fiscals.** Els endpoints fiscals agregats (Modelo 303 / 347 via `/v1/tax_reports/*`) retornen els seus imports com a **cèntims enters**, no com a decimals d'EUR — p. ex. una base imposable acumulada de `25000` significa `250.00 €`. Això està documentat camp a camp a la spec; tracta les xifres dels informes fiscals com a unitats menors i divideix per 100 només per mostrar-les. ## Dates [#dates] Les dates de calendari (sense component horari) usen **`YYYY-MM-DD`** — la forma de data completa [ISO-8601 / RFC 3339](https://en.wikipedia.org/wiki/ISO_8601). Això cobreix camps com ara `issued_on`, `due_on`, `paid_on`, `valid_until`, `delivery_date`, `received_on`, `start_on` i `end_on`: ```json { "issued_on": "2026-03-15", "due_on": "2026-04-14", "paid_on": "2026-03-20" } ``` Envia les dates en el mateix format. Una data no té hora ni zona horària — és el dia de calendari tal com queda registrat per al document. ## Timestamps [#timestamps] Els camps d'instant temporal (metadades d'auditoria i cicle de vida com ara `created_at`, `updated_at`, `signed_at`, `last_delivery_at`) usen strings de data-hora **ISO-8601 / RFC 3339** complets. La majoria s'emeten en **UTC** amb un sufix `Z`: ```json { "created_at": "2026-05-15T10:34:21Z" } ``` Alguns timestamps porten en el seu lloc un offset Europe/Madrid explícit (`+01:00` a l'hivern, `+02:00` a l'estiu): ```json { "created_at": "2026-04-15T10:31:05+02:00" } ``` Totes dues formes són ISO-8601 vàlides i denoten el mateix tipus de valor: un instant exacte. **Analitza l'offset** — no donis per fet que el string sempre està en UTC. Un parser ISO-8601 en condicions (`Instant.parse`, `datetime.fromisoformat`, `new Date(...)`, `Carbon::parse`) gestiona `Z` i `±hh:mm` de manera idèntica i normalitza a l'instant absolut. ## Zona horària per a les quotes [#zona-horària-per-a-les-quotes] La quota **mensual** del rate limit es reinicia el **dia 1 de cada mes natural a les `00:00` Europe/Madrid** (CET/CEST), no en UTC. La quota per minut és una finestra lliscant i la capçalera `X-RateLimit-Reset` és un **UNIX timestamp** (segons des de l'epoch, independent de la zona horària). Consulta [Rate limits](/guides/rate-limits) per a la semàntica completa de les finestres. Sempre que l'API necessita una única referència de calendari civil per a un límit de negoci — períodes fiscals, el reinici de la quota mensual — aquesta referència és **Europe/Madrid**. ## Referència ràpida [#referència-ràpida] | Valor | Format | Exemple | | ---------------------------- | ------------------------------------------------------------------ | ------------------------ | | Diners | EUR, dos decimals — string decimal (alguns camps emeten un número) | `"1234.56"` / `1802.9` | | Divisa | ISO 4217, sempre `EUR` a v1 | `"EUR"` | | Imports d'informes fiscals | **Cèntims** enters (unitats menors) | `25000` → 250.00 € | | Data | `YYYY-MM-DD` (ISO-8601 data completa) | `"2026-03-15"` | | Timestamp | ISO-8601 data-hora, normalment UTC `Z`, de vegades `±hh:mm` | `"2026-05-15T10:34:21Z"` | | Calendari de quotes / fiscal | Hora civil Europe/Madrid | dia 1, `00:00` CET/CEST | --- # Anul·lar o rectificar (/ca/guides/annul-vs-correct) Una factura emesa no es pot editar. Tot allò que sembla editar-la és en realitat una de quatre operacions diferents, cadascuna amb les seves condicions prèvies i la seva pròpia conseqüència davant l'Administració tributària. Aquesta pàgina és la taula de decisió, i el motiu de cada branca. ## Quan aplica [#when] Sempre que hi hagi alguna cosa malament en una factura i l'hagis de desfer. **L'estat actual de la factura acota les operacions legals; quan n'hi ha més d'una de legal, ho decideix la teva intenció.** La gravetat de l'error no hi entra mai. | Estat de la factura | Número assignat? | Operació | Conseqüència | | --------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `draft`, i no hi ha res que valgui la pena conservar | No | **Eliminar** — `DELETE /v1/invoices/{id}` | El registre desapareix. Mai no va ser fiscal. | | `draft`, però vols deixar constància de l'intent | No | **Cancel·lar** — canviar l'estat a `cancelled` | L'esborrany es retira però es conserva. | | `sent`, `overdue` — la factura no s'hauria d'haver emès mai | Sí | **Anul·lar** — `POST /v1/invoices/{id}/annul` | La factura deixa de ser cobrable i es declara una anul·lació a l'AEAT. | | `sent`, `paid` — la factura havia d'existir, el seu contingut és incorrecte | Sí | **Rectificar** — `POST /v1/invoices/{id}/corrective` | Un document fiscal nou que referencia l'original. | | `sent`, però només estava malament la marca de lliurament | Sí | **Desfer la marca de lliurament** — `POST /v1/invoices/{id}/unsend` | Es neteja la marca de lliurament. La factura continua emesa. | Tres regles fan que la taula sigui inequívoca: **Una factura numerada no s'elimina mai físicament.** Eliminar exigeix estat `draft` (o un `cancelled` que vingui d'un esborrany) **i** un número que continuï sent el provisional de l'esborrany. Tota factura que ha consumit un número de la seva sèrie queda protegida pel *soft-delete* fiscal; la via per retirar-la és l'anul·lació ([`BR-INV-002`](#traceability), art. 29.4 de la Llei general tributària sobre el deure de conservar els documents amb transcendència tributària). **Una factura pagada està tancada.** `paid` és terminal: el seu IVA repercutit s'ha declarat o es declararà en el període i el cobrament està identificat, de manera que anul·lar-la trencaria la traçabilitat i distorsionaria les declaracions d'IVA. El camí canònic és una rectificativa ([`BR-INV-023`](#traceability)). **En `sent` totes dues són legals — així que pregunta't què ha fallat.** Una rectificativa s'admet sobre `sent` o `paid` ([`BR-INV-001`](#traceability)) i una anul·lació sobre `sent` o `overdue` ([`BR-INV-003`](#traceability)): `sent` és l'únic estat en què l'API accepta qualsevol de les dues. L'estat no pot decidir per tu; la pregunta sí: * **La factura no hauria d'haver existit mai** — s'ha cancel·lat la comanda, ha anat al client equivocat, en duplica una altra → **anul·lar**. * **La factura s'havia d'emetre però el seu contingut és incorrecte** — import erroni, tipus d'IVA incorrecte, dades del destinatari malament, una devolució parcial → **rectificativa**. Si anul·les per un simple error d'import, declares una `ANULACION` a l'AEAT i cremes el número per no res; la rectificativa era el camí net i continua disponible en `sent`. ## Cancel·lar no és anul·lar [#cancel] Són actes diferents, i el domini els manté separats a propòsit. **Cancel·lar** retira un *esborrany* — un document que encara no obliga fiscalment. Només està disponible des de `draft`, i `cancelled` és terminal: un esborrany cancel·lat no es pot revifar, se'n crea un de nou ([`BR-INV-012`](#traceability)). **Anul·lar** retira una factura *emesa*. Només està disponible des de `sent` o `overdue`. No és una eliminació: la factura roman al llibre registre, en estat `annulled`, i si l'empresa està adherida a VeriFactu l'anul·lació es declara al seu torn. Intentar cancel·lar una factura emesa, o anul·lar un esborrany, respon `422` amb un error de transició no vàlida. Aquest codi és deliberat: es tracta d'una violació de regla de negoci, no d'un problema de permisos. ## Les rectificatives no s'anul·len [#corrective-annul] Una factura rectificativa no s'anul·la mai. Si la rectificativa mateixa està malament, emets una **rectificativa nova de la factura original** ([`BR-INV-003`](#traceability)). Intentar-ho respon `422`. El raonament és que tot el sentit d'una rectificativa és «aquest document modifica aquell». Anul·lar la modificació deixaria l'original en un estat ambigu davant l'Administració tributària, on tots dos documents ja estan registrats. ## `unsend` desfà una marca de lliurament, no una emissió [#unsend] `unsend` existeix per a un error concret: marcar com a lliurada una factura que no ho estava. Neteja la marca de lliurament i **manté l'estat en `sent`**. El número de sèrie, l'alta a l'AEAT i els *snapshots* congelats queden intactes, perquè una factura emesa és immutable ([`BR-INV-030`](#traceability), RD 1007/2023). Dues propietats importen per a les integracions: * És **idempotent**. Tornar-la a cridar quan la marca ja està neta és un no-op controlat, mai un `500`. * Està estrictament acotada a `sent`. Sobre una factura `paid`, `annulled`, `cancelled` o programada respon `422` — mai `403`. La matriu de transicions d'estat no conté cap camí de tornada de `sent` a `draft` per cap ruta, inclòs l'endpoint genèric de canvi d'estat. No existeix la «desemissió». ## Què envia l'API [#api] **Comprova primer.** [`GET /v1/invoices/{id}/can-annul`](/api-reference/invoices/public-api.v1.invoices.can_annul) (scope `invoices:read`) et dona la resposta abans de comprometre't, inclòs si l'anul·lació produirà un registre VeriFactu addicional: ```bash curl https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/can-annul \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ```json { "data": { "can_annul": false, "reasons": ["La factura está pagada."], "will_create_verifactu": false, "info": [] } } ``` **Després anul·la.** [`POST /v1/invoices/{id}/annul`](/api-reference/invoices/public-api.v1.invoices.annul) (scope `invoices:void`) deixa constància del motiu: ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/annul \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"reason": "El cliente cancela el pedido tras la emisión"}' ``` `POST /v1/invoices/{id}/void` arriba a la mateixa operació de domini sota el nom extern que el contracte de l'API fa servir per a l'estat (`voided`). Prefereix `annul` quan vulguis deixar constància del motiu; una segona crida sobre una factura ja anul·lada respon `422` en tots dos casos. Per al camí de la rectificativa —payload, codis de rectificació, herència de línies— consulta [Factures rectificatives](/guides/corrective-invoices). ## Què surt al PDF [#pdf] L'anul·lació no reescriu el document original. La factura conserva el seu número, les seves dades congelades de destinatari i emissor i el seu bloc QR; el que canvia és el seu estat al llibre registre i el fet que ara existeix una segona declaració davant l'AEAT. Eliminar un esborrany retira el document del tot — però un esborrany mai no va tenir número definitiu, ni *snapshot* congelat, ni QR, que és exactament per què eliminar és segur allà i enlloc més. `unsend` no canvia res del document imprès. Només neteja una marca de lliurament; la factura no torna a ser editable ([`BR-INV-030`](#traceability)). ## Què arriba a l'AEAT [#aeat] **L'anul·lació** d'una factura d'una empresa adherida a VeriFactu produeix un segon registre de facturació de classe `ANULACION`, encadenat a l'últim registre de l'empresa i referit a l'alta original ([`BR-VFC-014`](#traceability)). Es crea de manera asíncrona, després que la transacció confirmi, de manera que la factura arriba a `annulled` a la teva base de dades abans que es transmeti la declaració. Consulta el registre si necessites confirmar que l'AEAT l'ha acceptada — vegeu [Estats d'enviament VeriFactu](/guides/verifactu-submission-states). Hi ha un matís amb conseqüències reals. Si **l'alta original no es va acceptar mai** —està rebutjada, amb error, o encara pendent—, l'anul·lació ha de declarar explícitament que no existeix cap registre previ a l'AEAT. El sistema deriva aquesta marca de l'estat de l'alta en el moment en què es crea l'anul·lació i la persisteix com a *snapshot*, de manera que un canvi posterior de l'estat de l'original no desincronitza l'XML ja transmès. Sense aquesta marca, l'AEAT rebutja l'anul·lació de pla amb «el registre de facturació no existeix» ([`BR-VFC-026`](#traceability)). **Cancel·lar i eliminar un esborrany** no arriben a l'AEAT de cap manera: un esborrany no es va declarar mai. **Les rectificatives** són documents fiscals ordinaris i produeixen la seva pròpia alta, exactament igual que qualsevol altra factura. Si l'empresa no està adherida a VeriFactu, l'anul·lació funciona igualment i simplement no produeix cap declaració. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-INV-001` — una rectificativa exigeix un original en `sent` o `paid`; aquest solapament amb l'anul·lació en `sent` és la raó que allà hi decideixi la intenció i no l'estat. * `BR-INV-002` — *soft-delete* fiscal: una factura numerada no s'elimina mai físicament. * `BR-INV-003` — l'anul·lació es limita a factures emeses; les rectificatives no s'anul·len mai. * `BR-INV-012` — la cancel·lació es limita a esborranys i és terminal. * `BR-INV-023` — `paid` és un estat tancat; es corregeix amb una rectificativa, mai amb una anul·lació. * `BR-INV-030` — `unsend` neteja la marca de lliurament, manté la factura emesa, és idempotent i respon `422` en lloc de `403`. * `BR-VFC-014` — la classe de registre d'anul·lació i la cadena a què pertany. * `BR-VFC-026` — la marca de «sense registre previ» en l'anul·lació d'una alta que no es va acceptar mai. Derivat també de la màquina d'estats de factura documentada al costat d'aquestes regles, que és la font de veritat de les transicions citades a [Quan aplica](#when). --- # API keys (autoservei) (/ca/guides/api-keys) Més enllà del dashboard de desenvolupador, Factuarea exposa tot el cicle de vida de les teves API keys des de l'API pública v1, perquè aprovisionis i rotis credencials de manera programàtica. Cinc endpoints sota `/v1/account/api-keys` cobreixen llistar, crear, recuperar, rotar el secret i revocar — tots limitats a l'empresa autenticada. | Operació | Endpoint | Scope | | ----------------- | --------------------------------------------------- | --------------- | | Llistar keys | `GET /v1/account/api-keys` | `account:read` | | Crear una key | `POST /v1/account/api-keys` | `account:write` | | Recuperar una key | `GET /v1/account/api-keys/{api_key}` | `account:read` | | Rotar el secret | `POST /v1/account/api-keys/{api_key}/rotate_secret` | `account:write` | | Revocar una key | `POST /v1/account/api-keys/{api_key}/revoke` | `account:write` | `{api_key}` és l'`id` de la key — un UUID v7 opac, **no** el seu prefix ni el seu secret. Mira els esquemes complets a la [Referència de l'API](/api-reference/account/public-api.v1.account.api_keys.list). ## Llista les teves keys [#llista-les-teves-keys] `GET /v1/account/api-keys` retorna les teves keys amb [paginació per cursor](/guides/pagination). Cada key exposa el seu `prefix`, `scopes`, `tier`, `environment` i els timestamps del seu cicle de vida — mai el secret. ```bash curl https://api.factuarea.com/v1/account/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ```json { "data": [ { "object": "api_key", "id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f", "name": "Production sync", "prefix": "fact_live_8KqW3pXn", "scopes": ["invoices:read", "invoices:write"], "tier": "scale", "environment": "live", "active": true, "revoked": false, "last_used_at": "2026-06-23T18:04:11Z", "expires_at": null, "revoked_at": null } ], "has_more": false, "next_cursor": null } ``` `prefix` són els primers caràcters de la key — segur de registrar en logs, **no** autentica. Fes-lo servir per reconèixer una key als teus propis panells sense desar mai el secret. ## Crea una key [#create] `POST /v1/account/api-keys` emet una nova key i retorna el seu `secret` en text pla **exactament un cop**. Desa'l en el moment que el reps — no hi ha cap endpoint per tornar-lo a llegir més tard. ```bash curl -X POST https://api.factuarea.com/v1/account/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Reporting export", "scopes": ["invoices:read", "pdfs:read"], "environment": "test" }' ``` Resposta (`201`): ```json { "data": { "object": "api_key", "id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162", "name": "Reporting export", "prefix": "fact_test_1N0Fnyhh", "secret": "fact_test_1N0FnyhhR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "pdfs:read"], "tier": "scale", "environment": "test" } } ``` El camp `secret` apareix **només** en aquesta resposta `201` (i després d'una rotació). No el retorna mai el llistat, la recuperació ni cap altre endpoint. Si el perds has de rotar la key. Persisteix-lo en un gestor de secrets de seguida — mai en un log ni en un repositori. ### Cos de la petició [#cos-de-la-petició] | Camp | Obligatori | Notes | | ------------- | ---------- | ------------------------------------------------------------------------------ | | `name` | sí | Etiqueta llegible (1–120 caràcters). | | `scopes` | sí | Un o més [scopes](/guides/authentication#scopes) del catàleg tancat. Mínim un. | | `environment` | no | `live` (per defecte) o `test`. Vegeu [a sota](#environment). | | `expires_at` | no | Instant futur ISO 8601 a partir del qual la key deixa d'autenticar. | | `allowed_ips` | no | Llista opcional d'IPs / CIDR permeses (IPv4, IPv6, `/N`). | El `tier` es **deriva del pla de la teva empresa** (o d'un [boost de capacitat](/guides/rate-limits#capacity-boost) actiu quan és superior) — *no* es fixa des del cos. Si envies un `tier`, s'ignora. Demanar un scope fora del catàleg tancat, o un scope per sobre del teu pla, retorna `422` amb [errors per camp](/guides/errors). ## El camp environment [#environment] Cada key pertany a un dels dos environments, fixat en crear-la i visible a l'objecte de la key: | `environment` | Prefix | Opera sobre | | ------------- | ------------ | ----------------------------------------------------------------------------- | | `live` | `fact_live_` | La teva empresa real, amb efectes reals (VeriFactu → AEAT, emails, webhooks). | | `test` | `fact_test_` | Una empresa sandbox aïllada amb els efectes externs desactivats. | Passa `environment: test` en crear una key per emetre una credencial de sandbox; omet-lo per a una key de producció. El prefix reflecteix l'environment, així que els distingeixes sense descodificar la key. Mira [Mode de prova i sandbox](/guides/test-mode) per saber què es desactiva en `test`. ## Rota el secret [#rotate] `POST /v1/account/api-keys/{api_key}/rotate_secret` genera un nou `prefix` + `secret` i retorna el nou secret en text pla **exactament un cop**. El secret **anterior** continua funcionant durant una **finestra de gràcia de 24 hores** perquè puguis desplegar el nou sense downtime. ```bash curl -X POST \ https://api.factuarea.com/v1/account/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/rotate_secret \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ```json { "data": { "object": "api_key", "id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f", "prefix": "fact_live_Zq7mP4xV", "secret": "fact_live_Zq7mP4xVnR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "invoices:write"], "environment": "live" } } ``` Durant la finestra de gràcia de 24 hores autentiquen tant el nou com el secret anterior; una petició que segueixi usant l'anterior rep un header `199` `Warning` amb el compte enrere d'hores restants. En expirar la finestra el secret anterior es rebutja i es purga. Desplega el nou secret dins d'aquestes 24 hores. La rotació és irreversible. ## Revoca una key [#revoke] `POST /v1/account/api-keys/{api_key}/revoke` invalida una key de manera permanent. Les peticions posteriors autenticades amb ella fallen amb `401`. Un `reason` opcional (màx 500 caràcters) queda a l'audit log. ```bash curl -X POST \ https://api.factuarea.com/v1/account/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/revoke \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -d '{"reason": "Rotated out of the deploy pipeline"}' ``` La revocació és **irreversible** i **no** es limita a altres keys: pots revocar la pròpia key amb què estàs autenticant la petició, tallant el teu propi accés. Assegura't de tenir una altra key vàlida al seu lloc abans si encara necessites accés a l'API. Després de la revocació, les peticions amb aquesta key retornen `401` amb el codi **genèric** `invalid_api_key` — no un codi específic de «revocada»: ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "La API key proporcionada no es válida.", "request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV" } } ``` Això és **anti-enumeració** deliberada: l'API no revela mai si una key va ser revocada, ha caducat o no ha existit mai — tota key inutilitzable es veu igual per a un atacant. Ramifica la teva pròpia lògica segons el resultat `200`/`401`, no segons un codi específic de revocada. ## Scopes i aïllament [#scopes-i-aïllament] Els cinc endpoints estan protegits pels scopes d'`account`: * `account:read` — llistar i recuperar keys. * `account:write` — crear, rotar i revocar keys. Totes les operacions estan limitades a l'empresa autenticada. Un `id` de key que pertany a una altra empresa retorna `404 api_key_not_found` (de nou, anti-enumeració — no revela mai que la key existeix), mai `403`. Gestionar keys segueix requerint una key existent amb els scopes adequats. Crea la teva **primera** key al dashboard de desenvolupador ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)), i després fes servir aquests endpoints per aprovisionar la resta de manera programàtica. Mira [Autenticació](/guides/authentication) per al format de la key i les capçaleres. --- # Autenticació (/ca/guides/authentication) L'API de Factuarea autentica cada request amb una **API key**. Les claus són tokens opacs generats al dashboard de desenvolupadors ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)) i vinculats a una empresa concreta. Cada request a `https://api.factuarea.com/v1/*` ha d'incloure una clau vàlida en un dels dos formats admesos. ## Format de l'API key [#format-de-lapi-key] ``` fact_live_<24 alphanumeric characters> fact_test_<24 alphanumeric characters> ``` Exemple: ``` fact_live_8KqW3pXnR2VbY7TcA9eFmN5z fact_test_3pXnR2VbY7TcA9eFmN5z8KqW ``` * **Prefix**: determina l'**entorn**. `fact_live_` opera sobre la teva empresa real (producció); `fact_test_` opera sobre una empresa sandbox aïllada amb els efectes externs (VeriFactu → AEAT, FACe, emails, webhooks) desactivats. El prefix et permet identificar l'entorn sense descodificar la clau. Consulta [Mode de prova i sandbox](/guides/test-mode). * **Secret**: 24 caràcters base62 → \~143 bits d'entropia. Es mostra **només una vegada** en crear-la al dashboard. Si la perds, l'has de rotar. * **Hash a la BD**: el backend només emmagatzema el hash bcrypt cost-12 del secret. No hi ha cap manera de recuperar-lo. Tots els exemples d'aquesta guia usen una clau `fact_live_`, però el mateix request funciona amb una clau `fact_test_` — només cal canviar el prefix per operar sobre dades de sandbox. Crea i valida la teva integració primer en test. Consulta [Mode de prova i sandbox](/guides/test-mode). ## Enviar la clau a cada request [#enviar-la-clau-a-cada-request] L'API accepta dos formats equivalents. Tria el que millor encaixi amb el teu client: ### Authorization Bearer (recomanat) [#authorization-bearer-recomanat] ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ### Capçalera X-API-Key [#capçalera-x-api-key] ```bash curl https://api.factuarea.com/v1/clients \ -H "X-API-Key: fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Envia només una de les dues capçaleres. Si totes dues hi són presents, la capçalera `Authorization: Bearer` té prioritat. ## Exemples per llenguatge [#exemples-per-llenguatge] ```php $client = new GuzzleHttp\Client([ 'base_uri' => 'https://api.factuarea.com/v1/', 'headers' => [ 'Authorization' => 'Bearer ' . getenv('FACTUAREA_API_KEY'), 'Accept' => 'application/json', ], ]); $response = $client->get('clients?limit=10'); $body = json_decode((string) $response->getBody(), true); ``` ```javascript const res = await fetch('https://api.factuarea.com/v1/clients?limit=10', { headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, Accept: 'application/json', }, }); const data = await res.json(); ``` ```python import os import requests resp = requests.get( 'https://api.factuarea.com/v1/clients', params={'limit': 10}, headers={ 'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}", 'Accept': 'application/json', }, ) resp.raise_for_status() data = resp.json() ``` ## OAuth 2.1 [#oauth] Per a integracions d'agent i apps de tercers que actuen en nom d'un usuari de Factuarea, l'API també admet el **flux OAuth 2.1 authorization-code amb PKCE** (`code_challenge_method=S256`) com a alternativa a una API key estàtica. Els mateixos [scopes](#scopes) protegeixen l'access token, i la [política de rotació](#rotation-policy) aplica també als secrets de client OAuth. Les metadades de discovery (RFC 8414) es publiquen a `/.well-known/oauth-authorization-server`, perquè els clients OAuth resolguin els endpoints d'autorització i token automàticament: ```bash curl https://api.factuarea.com/.well-known/oauth-authorization-server ``` L'esquema de seguretat `OAuth2` — incloses les URL d'autorització i token i la llista completa de scopes — es descriu a la [Referència de l'API](/api-reference). ## Scopes [#scopes] Cada API key es crea amb un o més **scopes** que limiten quins endpoints pot invocar. Els scopes són cadenes amb la forma `:`. El catàleg és **tancat**: qualsevol scope fora del conjunt llistat provoca `invalid_scope` en crear la clau. ### Clients i catàleg [#clients-i-catàleg] | Scope | Permet | | ------------------ | ------------------------------- | | `clients:read` | Llistar i consultar clients. | | `clients:write` | Crear i actualitzar clients. | | `clients:delete` | Eliminar clients. | | `products:read` | Llistar i consultar productes. | | `products:write` | Crear i actualitzar productes. | | `products:delete` | Eliminar productes. | | `suppliers:read` | Llistar i consultar proveïdors. | | `suppliers:write` | Crear i actualitzar proveïdors. | | `suppliers:delete` | Eliminar proveïdors. | ### Documents de venda [#documents-de-venda] | Scope | Permet | | ---------------------------- | --------------------------------------------------------------- | | `invoices:read` | Llistar i consultar factures. | | `invoices:write` | Crear i actualitzar factures (inclou duplicar i rectificativa). | | `invoices:delete` | Eliminar esborranys de factura. | | `invoices:send` | Enviar factura per email al client. | | `invoices:void` | Anul·lar una factura emesa. | | `quotes:read` | Llistar i consultar pressupostos. | | `quotes:write` | Crear i actualitzar pressupostos. | | `quotes:delete` | Eliminar pressupostos. | | `quotes:send` | Enviar pressupost per email. | | `quotes:transition` | Acceptar, rebutjar o convertir pressupostos. | | `proformas:read` | Llistar i consultar factures proforma. | | `proformas:write` | Crear i actualitzar factures proforma. | | `proformas:delete` | Eliminar factures proforma. | | `proformas:send` | Enviar factura proforma per email. | | `proformas:transition` | Convertir factura proforma en factura. | | `delivery_notes:read` | Llistar i consultar albarans. | | `delivery_notes:write` | Crear i actualitzar albarans. | | `delivery_notes:delete` | Eliminar albarans. | | `delivery_notes:transition` | Marcar com a lliurat/cancel·lat, signar, convertir. | | `delivery_notes:gdpr_forget` | Esborrar la PII d'auditoria de signatura (RGPD Art. 17). | ### Compres i recurrents [#compres-i-recurrents] | Scope | Permet | | ------------------------------- | --------------------------------------------- | | `purchase_invoices:read` | Llistar i consultar factures de compra. | | `purchase_invoices:write` | Crear i actualitzar factures de compra. | | `purchase_invoices:delete` | Eliminar factures de compra. | | `purchase_invoices:transition` | Marcar com a pagada, rebuda, comptabilitzada. | | `recurring_invoices:read` | Llistar i consultar plantilles recurrents. | | `recurring_invoices:write` | Crear i actualitzar plantilles recurrents. | | `recurring_invoices:delete` | Eliminar plantilles recurrents. | | `recurring_invoices:transition` | Pausar, reprendre i emetre manualment. | ### Catàlegs i exportació [#catàlegs-i-exportació] | Scope | Permet | | ------------------- | ------------------------------------------------------------------------------------------------------------ | | `taxes:read` | Llegir el catàleg (global) de tipus impositius. | | `taxes:write` | Crear i actualitzar tipus impositius. | | `taxes:delete` | Eliminar tipus impositius. | | `series:read` | Llistar sèries de numeració de factures. | | `series:write` | Crear i actualitzar sèries de numeració de factures. | | `pdfs:read` | Descarregar PDFs de qualsevol document amb el scope `:read` corresponent. | | `tax_reports:read` | Llegir informes fiscals (Modelo 303/347, etc.). | | `tax_reports:write` | Generar informes fiscals. | | `account:read` | Llegir el compte autenticat (`GET /v1/account`). | | `account:write` | Gestionar les API keys del propi compte (crear, rotar, revocar) i actualitzar la personalització del compte. | ### VeriFactu i FacturaE [#verifactu-i-facturae] | Scope | Permet | | ----------------- | ----------------------------------------------------------------------------- | | `verifactu:read` | Llegir registres, esdeveniments, certificats i configuració de VeriFactu. | | `verifactu:write` | Gestionar certificats, ajustos i reintents de VeriFactu. | | `facturae:read` | Descarregar l'XML FacturaE d'una factura i llegir els seus enviaments a FACe. | | `facturae:write` | Enviar factures a FACe i sol·licitar l'anul·lació d'enviaments. | ### Webhooks i esdeveniments [#webhooks-i-esdeveniments] | Scope | Permet | | ----------------- | -------------------------------------------------------------- | | `webhooks:read` | Llistar webhook endpoints i lliuraments. | | `webhooks:write` | Crear, actualitzar, rotar i fer ping a webhook endpoints. | | `webhooks:delete` | Eliminar webhook endpoints. | | `events:read` | Llegir el catàleg d'esdeveniments i esdeveniments individuals. | ### Empreses gestionades (gestoria) [#empreses-gestionades-gestoria] Scopes detallats per al model de **gestoria**, on un compte mestre gestiona empreses filles i les seves API keys. Només accessibles amb API key (sense equivalent al consentiment OAuth); `companies:*` requereix a més el mòdul del pla de gestoria. | Scope | Permet | | ------------------ | ---------------------------------------------------------------- | | `companies:read` | Llistar i consultar les empreses gestionades (subcomptes fills). | | `companies:write` | Crear, actualitzar, activar i desactivar empreses gestionades. | | `companies:delete` | Arxivar empreses gestionades. | | `api_keys:read` | Llistar i consultar les API keys de les empreses gestionades. | | `api_keys:write` | Crear, rotar i revocar les API keys de les empreses gestionades. | | `api_keys:delete` | Eliminar permanentment les API keys de les empreses gestionades. | ### Super-scope [#super-scope] | Scope | Permet | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `*` | Accés total — equivalent a tenir tots els altres scopes anteriors. Reservat per a claus d'owner / migracions puntuals. **Evita usar-lo en integracions de producció**. | Si un request usa un endpoint que requereix un scope no concedit a la clau, la resposta és `403` amb `type: authorization_error` i `code: insufficient_scope`. ```json { "error": { "type": "authorization_error", "code": "insufficient_scope", "message": "La API key no tiene el scope requerido para esta operación.", "request_id": "req_01JBVH7..." } } ``` ## Gestió de claus [#gestió-de-claus] Les API keys es gestionen des del dashboard de desenvolupadors ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)), no a través de l'API pública. Des d'allà pots crear claus, rotar el seu secret, revocar-les, configurar scopes, un `expires_at` opcional i una llista d'accés per IP. Les metadades de la clau autenticada (id, name, prefix, scopes, tier, `last_used_at`, `expires_at`) es poden llegir via `GET /v1/account` — però el secret **mai** es retorna. **No hi ha cap endpoint per "veure" el secret**. Només es mostra una vegada en crear-lo. Si perds el valor has de rotar la clau al dashboard i tornar a desplegar el nou secret. És deliberat: minimitza la finestra d'exposició. ### Política de rotació [#rotation-policy] Les API keys i els secrets de client OAuth són credencials de llarga vida i s'han de rotar en un calendari i de seguida després de qualsevol sospita de filtració. * Els **prefixos** són la font de veritat de l'entorn: `fact_live_` (producció) i `fact_test_` (sandbox). No els barregis mai entre entorns. * **Rota** des del dashboard (o mitjançant els [endpoints self-service `account:write`](/guides/api-keys#rotate)) per emetre un secret nou. El nou secret es retorna **una sola vegada** — desa'l de seguida, no es torna a mostrar. * **Finestra de gràcia (doble secret).** Després d'una rotació el secret anterior continua funcionant durant una **finestra de gràcia de 24 hores**, perquè puguis desplegar el nou secret sense temps d'inactivitat. Durant aquesta finestra s'accepten tant el nou com l'anterior; en expirar la finestra el secret anterior es rebutja i es purga. Un request que continuï usant el secret anterior rep un header `199` `Warning` que indica quantes hores queden abans que deixi de funcionar. * **Quan rotar**: en un calendari regular (p. ex. cada 90 dies), sempre que un membre de l'equip amb accés marxi, i **immediatament** si un secret queda exposat alguna vegada en logs, control de versions o un client públic. * **Revoca** per invalidar una clau permanentment. Qualsevol request posterior amb ella falla amb `401`. La revocació no té finestra de gràcia — és instantània i irreversible. Els secrets estan lligats a una sola empresa (tenant) i no han d'incrustar-se mai en navegadors, apps mòbils ni cap client públic — mantén-los només al servidor. ### Llista d'accés per IP [#llista-daccés-per-ip] Cada API key es pot restringir a una llista d'IPs o rangs CIDR des del dashboard. Si el request arriba des d'una IP fora de la llista d'accés, la resposta és `401` i l'incident es registra al log d'auditoria. Deixa la llista d'accés buida per permetre qualsevol IP. ## Errors d'autenticació [#errors-dautenticació] Les fallades relacionades amb l'API key responen amb HTTP `401` (o `403` per a `insufficient_scope`) i l'embolcall d'error estàndard. El camp `code` distingeix el cas: | `code` | HTTP | Causa | | ------------------------ | ----- | ------------------------------------------------------------------------------------------------ | | `missing_api_key` | `401` | No s'ha enviat cap capçalera d'autenticació. | | `invalid_api_key` | `401` | La clau no existeix, té un format incorrecte o el secret no coincideix amb el hash emmagatzemat. | | `api_key_revoked` | `401` | La clau va ser revocada o ha caducat. Crea'n una de nova al dashboard. | | `too_many_auth_failures` | `429` | Massa intents d'autenticació fallits; espera abans de reintentar. | | `insufficient_scope` | `403` | La clau no té el scope que requereix l'endpoint. | Cada resposta inclou un `request_id` únic (també a la capçalera `X-Request-Id`) que pots facilitar a suport en investigar. ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "La API key proporcionada no es válida.", "request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV", "doc_url": "https://docs.factuarea.com/guides/errors#invalid_api_key" } } ``` ## Bones pràctiques [#bones-pràctiques] * **Mai** pugis API keys a repositoris — usa variables d'entorn o un gestor de secrets (AWS Secrets Manager, Doppler, 1Password Service Accounts). * Crea **una clau per integració**: facilita rotar i auditar l'accés sense afectar la resta. * Limita els scopes al mínim necessari. Un script d'exportació només necessita scopes `:read` concrets. * Activa la llista d'accés per IP per a integracions servidor a servidor amb IPs estables. * Configura `expires_at` per a claus temporals (p. ex. consultories, demos). * Audita l'ús des del dashboard: `Developers > API Keys > Activity` mostra IPs, rutes i errors per clau. --- # Operacions en lot (/ca/guides/bulk-operations) Els endpoints bulk processen diverses files en una sola petició i **mai no fan fallar tot el lot perquè una fila es rebutgi**. Cada fila s'avalua de manera independent i la resposta informa, fila a fila, de si s'ha aplicat o no. És el contracte d'**èxit parcial** (partial-success), compartit per tots els endpoints bulk de l'API pública. La superfície bulk ara abasta operacions de **delete, create, pdf, send i status** sobre els recursos de document i de catàleg — no només la família original `bulk-delete`. Algunes retornen la forma `BulkPartialSuccessResult` de sota, `bulk-create` retorna la forma més rica `BulkCreateResult`, i `bulk-pdf` transmet un ZIP binari en lloc de l'embolcall JSON. Totes honoren l'èxit parcial: una fila dolenta mai no enfonsa el lot. ## Forma de la resposta [#forma-de-la-resposta] Una operació bulk sempre retorna `200 OK` amb un `BulkPartialSuccessResult` dins de `data`: ```json { "data": { "total": 3, "successful": 2, "failed": 1, "failures": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "error_code": "resource_not_deletable", "error_message": "La factura ya está emitida y no se puede eliminar." } ] } } ``` | Camp | Tipus | Significat | | ------------ | ------- | ----------------------------------------------------------------------------------------------- | | `total` | integer | Files processades (`successful + failed`). | | `successful` | integer | Files aplicades (eliminades, creades o validades). | | `failed` | integer | Files que no s'han pogut processar. Igual a la longitud de `failures`. | | `failures` | array | Un element per cada fila fallida. Sempre una llista — buida, mai `null`, quan no ha fallat res. | L'invariant `total === successful + failed` i `failed === failures.length` es compleix sempre. Un lot totalment correcte retorna `failures: []`. ## Un element d'error [#un-element-derror] Cada entrada de `failures` identifica la fila i explica per què s'ha rebutjat. La identitat és **polimòrfica**: * `id` — l'UUID d'un recurs existent (bulk-delete i altres operacions sobre recursos existents). * `index` — la posició (base 0) de la fila al lot, per a files noves que encara no tenen recurs (bulk-create amb `dry_run`, import CSV). Exactament un d'`id` / `index` és present. | Camp | Tipus | Significat | | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | string | UUID v7 del recurs existent que no s'ha pogut processar. | | `index` | integer | Posició (base 0) de la fila dins del lot. | | `error_code` | string | Codi llegible per màquina del catàleg d'errors v1 (estable entre idiomes). Ramifica segons això. | | `error_message` | string | Motiu llegible per humans, en castellà. Per mostrar, no per ramificar. | | `errors` | array | Problemes bloquejants per camp (`FieldIssue[]`). Presents en fluxos `validate-only` / `bulk-create`; absents en `bulk-delete`. | | `warnings` | array | Avisos no bloquejants per camp (`FieldIssue[]`). | Ramifica segons **`error_code`**, mai segons `error_message` — el missatge és text en castellà orientat a persones i pot canviar. Per a bulk-delete els codis són `resource_not_found` (l'UUID no existeix o pertany a una altra empresa) i `resource_not_deletable` (el recurs existeix però el seu estat impedeix eliminar-lo: un albarà signat, un pressupost facturat, un client amb documents, etc.). ## Llegir el resultat [#llegir-el-resultat] No tractis la crida com a tot-o-res. Inspecciona `failures` i actua fila a fila: ```bash curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-delete \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02", "01931b3e-...a03"] }' \ | jq '.data | {total, successful, failed, failures}' ``` ```ts const { data } = await factuarea.quotes.bulkDelete({ ids }); if (data.failed > 0) { for (const f of data.failures) { // f.id, f.error_code, f.error_message console.warn(`${f.id} → ${f.error_code}: ${f.error_message}`); } } ``` ```python res = factuarea.quotes.bulk_delete(ids=ids) data = res["data"] for f in data["failures"]: # ramifica segons error_code, mostra error_message print(f["id"], f["error_code"], f["error_message"]) ``` ## UUID aliens i desconeguts [#uuid-aliens-i-desconeguts] Els UUID que no pertanyen a la teva empresa, o que no existeixen, **mai no són un 404 global**. El handler filtra per `company_id`, així que un UUID alien o desconegut es reporta com un error normal (`resource_not_found`) — mai no revela si un recurs existeix en un altre tenant. ## Bulk create (només validar amb dry\_run) [#bulk-create-només-validar-amb-dry_run] `bulk-create` accepta fins a **100** files per a factures i fins a **500** per a clients en una sola crida, i retorna un embolcall més ric, `BulkCreateResult`. El flag `dry_run` (per defecte `false`) alterna entre dos comportaments: * **`dry_run: true`** valida cada fila **sense persistir res** i retorna un `results[]` per fila. Cada entrada porta el seu `index`, un `status`, i els `errors[]` / `warnings[]` trobats per a aquella fila. No s'escriu res — fes-lo servir per mostrar els problemes a la teva interfície abans de confirmar. * **`dry_run: false`** crea **només les files vàlides**. Les files que fallen la validació no es creen i tornen a `failures[]`, cadascuna identificada pel seu `index` (base 0). L'embolcall porta tots dos arrays, així que el mateix parser serveix en qualsevol mode: ```json { "data": { "dry_run": true, "total": 2, "successful": 1, "failed": 1, "results": [ { "index": 0, "status": "valid", "errors": [], "warnings": [] }, { "index": 1, "status": "invalid", "errors": [{ "field": "client_id", "code": "required", "message": "El cliente es obligatorio." }], "warnings": [] } ], "failures": [ { "index": 1, "error_code": "validation_failed", "error_message": "Faltan campos obligatorios en la fila." } ] } } ``` Valida primer amb `dry_run: true`, corregeix el que marqui `results[]` i torna a enviar el mateix payload amb `dry_run: false` per persistir les files que passen: ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-create \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9f1c2b7a-0e44-4c1a-8f3d-1a2b3c4d5e6f" \ -d '{ "dry_run": true, "invoices": [ { "client_id": "01931b3e-...c01", "lines": [{ "description": "Consulting", "quantity": 1, "unit_price": "100.00", "tax_rate_id": "01931b3e-...t21" }] }, { "lines": [{ "description": "Missing client", "quantity": 1, "unit_price": "50.00" }] } ] }' | jq '.data | {dry_run, total, successful, failed, results, failures}' ``` ## Bulk PDF (descàrrega ZIP) [#bulk-pdf-descàrrega-zip] `bulk-pdf` empaqueta els PDF de fins a **50** documents en un únic ZIP i transmet de tornada l'**arxiu binari** — **no** retorna l'embolcall JSON. Els id que no es troben, o que no tenen PDF disponible, **no** aborten la petició: el ZIP porta només els documents vàlids, i els recomptes per id viatgen a les capçaleres de resposta `X-Bulk-*` perquè puguis conciliar què va entrar. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-pdf \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"] }' \ -D - -o invoices.zip ``` El flag `-D -` bolca les capçaleres de resposta: llegeix `X-Bulk-Total`, `X-Bulk-Successful` i `X-Bulk-Failed` per saber quants id van entrar a l'arxiu. ## Bulk send (enviament en lot) [#bulk-send-enviament-en-lot] `bulk-send` encua fins a **200** documents per enviar-los per email i retorna la forma `BulkPartialSuccessResult`. L'enviament és asíncron: una fila `successful` significa que l'email es va **encuar**, no que ja s'ha entregat. Els camps opcionals `to`, `cc`, `subject`, `message` i `language` sobreescriuen els valors per defecte per a tot el lot. ```bash curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-send \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c3e1f90-2a11-4b22-9d44-5e6f7a8b9c0d" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "language": "es" }' \ | jq '.data | {total, successful, failed, failures}' ``` ## Transicions d'estat en lot [#transicions-destat-en-lot] `bulk-status` mou fins a **50** documents a un nou estat, i cada transició passa **per la guarda de l'Aggregate** — una fila l'estat actual de la qual prohibeix el moviment falla de manera individual i cau a `failures[]`, mentre que la resta sí que transiciona. El `new_status` destí ha de pertànyer al conjunt tancat permès per a aquell recurs (consulta la taula de sota). Per a factures, `payment_date` és **obligatori** quan `new_status` és `paid`. Per a factures de compra, `payment_date` també és obligatori i es propaga **tal qual** — mai no es reemplaça en silenci per `now()`. Per a productes i proveïdors la transició és **idempotent**: un recurs que ja està en l'estat sol·licitat compta com a `successful` sense canviar res. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-status \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3b8d2e10-4f55-4c66-8a77-9b0c1d2e3f40" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "new_status": "paid", "payment_date": "2026-03-20" }' \ | jq '.data | {total, successful, failed, failures}' ``` Els valors `new_status` permesos per recurs: | Recurs | `new_status` permès | | ------------------- | --------------------------------- | | `invoices` | `sent`, `paid` | | `quotes` | `approved`, `rejected` | | `proformas` | `accepted`, `rejected` | | `delivery_notes` | `delivered`, `cancelled` | | `purchase_invoices` | `paid` | | `products` | `active`, `inactive` (idempotent) | | `suppliers` | `active`, `inactive` (idempotent) | ## Endpoints i límits [#endpoints-i-límits] Cada operació limita el lot a un nombre fix de files. Trossejar una feina més gran en blocs dins d'aquests límits queda de la teva part: | Operació | Recursos | Files màx. | Forma de resposta | | ------------- | ------------------------------------------------------------------------------------------------- | ---------- | -------------------------- | | `bulk-create` | `invoices` (100), `clients` (500) | 100 / 500 | `BulkCreateResult` | | `bulk-pdf` | `invoices`, `quotes`, `proformas`, `delivery_notes` | 50 | ZIP binari + `X-Bulk-*` | | `bulk-send` | `invoices`, `quotes`, `proformas`, `delivery_notes` | 200 | `BulkPartialSuccessResult` | | `bulk-status` | `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices`, `products`, `suppliers` | 50 | `BulkPartialSuccessResult` | | `bulk-delete` | els nou recursos | — | `BulkPartialSuccessResult` | **Fes servir `Idempotency-Key` a les operacions bulk que muten.** `bulk-create`, `bulk-send`, `bulk-status` i `bulk-delete` accepten totes la capçalera `Idempotency-Key`, així que un reintent després d'una connexió caiguda repeteix el resultat original en lloc d'executar el lot dues vegades. `bulk-pdf` és una lectura pura i no necessita clau. ## Versionat — la forma legacy [#versionat--la-forma-legacy] La forma d'èxit parcial és el contracte actual. Els integradors **ancorats a una versió anterior a `2026-09-01`** (mitjançant el header `Factuarea-Version` o un pin a l'API key) continuen rebent la forma anterior de bulk-delete, de manera que cap integració existent no es trenca: ```json { "object": "bulk_delete_result", "deleted": 2, "failed": [ { "id": "01931b3e-...a01", "reason": "La factura ya está emitida y no se puede eliminar." } ] } ``` El mapatge entre ambdues formes és mecànic: `deleted` és el nou `successful`, i cada `failed[].reason` legacy és el nou `failures[].error_message` (la nova forma hi afegeix a sobre l'`error_code` estable i el comptador `total`). No enviïs header —o envia una data igual o posterior a `2026-09-01`— per obtenir la forma d'èxit parcial. Ancora una versió només per congelar un contracte del qual ja depens. Les integracions noves haurien d'usar la forma d'èxit parcial: porta un `error_code` estable i independent de l'idioma pel qual ramificar, cosa que la cadena `reason` legacy no ofereix. --- # Verificació censal AEAT (/ca/guides/census-verification) Factuarea pot comprovar que el parell **raó social + NIF** de la teva empresa està correctament identificat al **cens de l'AEAT** — la mateixa identificació que fa l'AEAT quan rep els teus registres de facturació VeriFactu. Un parell no censat provoca el rebuig de l'enviament (error AEAT 4104, *titular no identificat*), així que verificar **aviat** — just després del registre i cada cop que canviïn les teves dades fiscals — t'estalvia enviaments rebutjats després. ``` POST /v1/account/census-verification ``` * **Scope:** `account:read` * **Body de la request:** cap — la comprovació s'executa sempre contra la raó social i el `tax_id` **persistits** del compte autenticat. Mai no accepta un NIF o nom arbitrari al payload. * **Efecte:** el resultat es desa com a snapshot a la teva empresa (visible als ajustos de l'app de Factuarea). Canviar la raó social o el `tax_id` reinicia l'snapshot fins que tornis a verificar. Aquesta és la mateixa verificació que Factuarea ofereix a l'app durant l'onboarding. Els resultats negatius són **informatius**: mai no bloquegen el registre, la facturació ni cap altra operació — només t'avisen que els enviaments VeriFactu poden ser rebutjats fins que corregeixis les dades censals. ## Cridar l'endpoint [#cridar-lendpoint] ```bash curl -X POST https://api.factuarea.com/v1/account/census-verification \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` Resposta (`200`): ```json { "data": { "object": "census_verification", "status": "identified", "verified_name": "ACME SOLUTIONS SL", "checked_at": "2026-06-10T22:15:04+00:00" } } ``` `verified_name` és la raó social que es va contrastar contra el cens (la persistida). `checked_at` és el moment de la verificació, en ISO 8601. Amb els SDK oficials: ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); const result = await factuarea.account.verifyCensus(); // result.data.status → "identified" | "not_identified" | … ``` ```php account->publicApiV1AccountVerifyCensus(); ``` ## Verifica també els teus clients [#clients] L'AEAT executa la mateixa identificació sobre el **destinatari** de cada registre de facturació VeriFactu: un client el parell nom + NIF del qual no està censat provoca el rebuig de l'enviament amb l'error AEAT **1239** (*destinatari no identificat*). Factuarea **no valida deliberadament els clients contra el cens en crear-los** — i és per disseny, no un descuit: els clients estrangers no tenen entrada al cens espanyol, les empreses acabades de constituir poden trigar dies a aparèixer, les factures simplificades B2C no porten NIF de destinatari, i el mateix servei de l'AEAT pot estar caigut (tota la funcionalitat és fail-open). Bloquejar la creació de clients pel cens trencaria tots aquests fluxos legítims. El patró recomanat és un altre: **verifica el parell nom + NIF just abans d'emetre factures VeriFactu a aquest client** amb l'endpoint dedicat: ``` POST /v1/clients/census-verification ``` * **Scope:** `clients:read` * **Body de la request:** `tax_id` (NIF/CIF/NIE) i `name` — el parell es comprova **conjuntament**, exactament igual que ho comprovarà l'AEAT en l'enviament. No cal que correspongui a un client existent: la comprovació és **stateless** i no persisteix res als teus clients. * **Límit de peticions:** 5 verificacions per minut, com l'endpoint del compte. ```bash curl -X POST https://api.factuarea.com/v1/clients/census-verification \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"tax_id": "B12345674", "name": "CONSTRUCCIONES PEREZ SL"}' ``` Resposta (`200`): ```json { "data": { "object": "census_verification", "status": "identified", "verified_name": "CONSTRUCCIONES PEREZ SL", "checked_at": "2026-06-11T09:30:00Z" } } ``` Els valors de `status` són els mateixos sis que a la verificació del compte (taula més avall). Com actuar-hi per a un client: * `identified` — emet amb normalitat. * `not_identified` — un enviament VeriFactu a aquest destinatari té el **rebuig 1239 garantit**. Demana al client la seva raó social exacta i el seu NIF abans d'emetre. * `not_identified_similar` — (persones físiques) fes servir el nom exacte tal com està registrat a l'AEAT. * `unavailable` — l'AEAT no va poder respondre; la comprovació és informativa, així que pots emetre igualment i reintentar la verificació més tard. Si tot i això es cola un enviament rebutjat, no està tot perdut: l'[esmena](/guides/verifactu-subsanacion) et permet corregir les dades i reenviar el mateix registre. Verificació censal per davant més esmena com a xarxa de seguretat cobreixen el cicle complet del 1239. ## Estats possibles [#estats-possibles] `status` és sempre un d'aquests sis valors: | `status` | Significat | Què fer | | ------------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `identified` | El parell raó social + NIF coincideix amb el cens. | Res — estàs a punt per a VeriFactu. | | `not_identified` | El NIF no està identificat al cens amb aquesta raó social. | Revisa les teves dades fiscals: raó social exacta i NIF. Els enviaments corren risc de rebuig. | | `not_identified_similar` | (Només persones físiques.) El NIF existeix però el nom només coincideix parcialment. | Fes servir el nom exacte tal com està registrat a l'AEAT. | | `identified_inactive` | El NIF està identificat però figura de baixa al cens. | Comprova la teva situació censal amb l'AEAT. | | `identified_revoked` | El NIF està identificat però ha estat revocat. | Comprova la teva situació censal amb l'AEAT. | | `unavailable` | No s'ha pogut contactar amb el servei de l'AEAT o ha retornat una resposta no reconeguda. | Reintenta més tard. **No** és un error. | Ramifica segons `status`, els valors són un contracte congelat. Tingues en compte que **els estats negatius no són errors HTTP**: tota verificació completada retorna `200`. ## Fail-open per disseny [#fail-open-per-disseny] La verificació mai no trenca el teu flux perquè l'AEAT estigui caiguda: * Timeout de l'AEAT, SOAP fault o resposta no reconeguda → `200` amb `status: unavailable`. Mai un `5xx` per aquesta causa. * Els resultats es cachegen al servidor durant un període curt, de manera que els reintents immediats del mateix parell no tornen a cridar l'AEAT (`unavailable` es cacheja només uns segons perquè puguis reintentar aviat). ## Errors [#errors] L'únic error de negoci és una empresa sense dades fiscals: ```json { "error": { "type": "invalid_request_error", "code": "census_requires_tax_id", "message": "Configura primero los datos fiscales de tu empresa para verificar el censo.", "param": "tax_id" } } ``` | HTTP | `code` | Quan | | ---- | ------------------------------------- | ------------------------------------------- | | 401 | `missing_api_key` / `invalid_api_key` | API key absent o invàlida. | | 403 | `insufficient_scope` | La clau no té l'scope `account:read`. | | 422 | `census_requires_tax_id` | El compte encara no té `tax_id` configurat. | | 429 | `rate_limit_exceeded` | Més de **5 verificacions per minut**. | Consulta la [guia de l'embolcall d'error](/guides/errors) per al contracte complet d'errors. ## Límit de peticions [#límit-de-peticions] L'endpoint està limitat a **5 verificacions per minut** per compte, independentment del tier global de límit de peticions de la teva clau. Per sobre reps un `429` — espera que la finestra es reiniciï i reintenta. ## Mode de prova: NIFs màgics [#mode-de-prova-nifs-màgics] Amb una clau `fact_test_` ([mode de prova](/guides/test-mode)) la verificació **mai no arriba a l'AEAT**. El sandbox retorna estats deterministes segons el `tax_id` de l'empresa sandbox, perquè puguis exercitar totes les branques de la teva integració: | `tax_id` del sandbox | `status` retornat | | -------------------- | -------------------------- | | `00000000T` | `identified` | | `11111111H` | `not_identified` | | `22222222J` | `not_identified_similar` | | `33333333P` | `identified_inactive` | | `44444444A` | `identified_revoked` | | `55555555K` | `unavailable` | | qualsevol altre NIF | `identified` (per defecte) | Tots els NIFs màgics porten una lletra de control vàlida, així que passen la validació estàndard de NIF. Configura el `tax_id` de l'empresa sandbox amb el valor màgic que vulguis provar i crida l'endpoint amb la teva clau `fact_test_`: ```bash curl -X POST https://api.factuarea.com/v1/account/census-verification \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` ```json { "data": { "object": "census_verification", "status": "identified_revoked", "verified_name": "SANDBOX COMPANY SL", "checked_at": "2026-06-10T22:15:04+00:00" } } ``` En producció es consulta el cens real de l'AEAT amb el certificat de plataforma de Factuarea. Els estats reflecteixen la resposta de l'AEAT al peu de la lletra — Factuarea mai no inventa un estat. --- # API keys d'empreses filles (/ca/guides/child-api-keys) Cada [empresa gestionada](/guides/companies) té el seu propi joc d'API keys. Una key filla autentica peticions **en nom d'aquesta única empresa** — mai arriba a les empreses germanes ni al tenant mestre. És l'alternativa a manejar una filla amb el [header `X-Active-Profile`](/guides/acting-on-behalf): una key filla queda lligada a una empresa per sempre, en comptes de canviar-se per petició. Cinc endpoints sota `/v1/companies/{id}/api-keys` cobreixen el seu cicle de vida. | Operació | Endpoint | Scope | | ----------------------- | ------------------------------------------------------ | ---------------- | | Llistar keys filles | `GET /v1/companies/{id}/api-keys` | `api_keys:read` | | Crear una key filla | `POST /v1/companies/{id}/api-keys` | `api_keys:write` | | Recuperar una key filla | `GET /v1/companies/{id}/api-keys/{key}` | `api_keys:read` | | Rotar el secret | `POST /v1/companies/{id}/api-keys/{key}/rotate-secret` | `api_keys:write` | | Revocar una key filla | `DELETE /v1/companies/{id}/api-keys/{key}` | `api_keys:write` | Això replica l'autoservei d'[API keys](/guides/api-keys) a nivell de compte, però acotat a una empresa filla en comptes del teu propi compte. Tant `{id}` (l'empresa) com `{key}` (l'API key) són valors UUID v7 opacs. ## Crea una key filla [#create] `POST /v1/companies/{id}/api-keys` emet una key per a l'empresa i retorna el seu `secret` en text pla **exactament un cop**. Requereix el scope `api_keys:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Producción Talleres García", "scopes": ["invoices:read", "invoices:write"] }' ``` Resposta (`201`): ```json { "data": { "object": "api_key", "id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162", "name": "Producción Talleres García", "prefix": "fact_live_1OSf9KdP", "secret": "fact_live_1OSf9KdPR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "invoices:write"], "tier": "scale", "environment": "live" } } ``` El `secret` es mostra **només** en aquesta resposta `201` i després d'una rotació. Cap endpoint el retorna després. Persisteix-lo en un gestor de secrets tan bon punt el reps — mai en un log ni en un repositori. Si el perds, rota la key. ### Els scopes han de ser un subconjunt de la key pare [#subset] Els scopes que demanes per a una key filla **han de ser un subconjunt dels de la key que fa la crida**. Demanar un scope que la key que crida no té retorna `422` amb errors per camp — **sense retallada silenciosa**: la key no es crea amb una llista de scopes escurçada, falla la petició sencera. ```json { "error": { "type": "validation_error", "code": "validation_failed", "message": "No puedes conceder un scope que tu propia key no tiene.", "param": "scopes" } } ``` Així, una key amb `invoices:read invoices:write` pot emetre keys filles amb qualsevol subconjunt d'aquests dos scopes, però mai amb `clients:write`. Aprovisiona primer una key mestra amb scopes suficients i deriva'n keys filles més estretes. L'`environment` i el `tier` mai es prenen del cos — s'hereten de la key que crida. | Camp | Obligatori | Notes | | -------------- | ---------- | ----------------------------------------------------------------------------------------------- | | `name` | sí | Etiqueta llegible (1–120 caràcters). | | `scopes` | sí | Un o més [scopes](/guides/authentication#scopes), cadascun subconjunt dels de la key que crida. | | `expires_at` | no | Instant futur ISO 8601 a partir del qual la key deixa d'autenticar. | | `ip_allowlist` | no | Llista opcional d'IPs / CIDR permeses (IPv4, IPv6, `/N`). | ## Rota el secret [#rotate] `POST /v1/companies/{id}/api-keys/{key}/rotate-secret` invalida de seguida el secret actual, genera un nou `prefix` + `secret`, i retorna el nou secret en text pla **exactament un cop**. Requereix el scope `api_keys:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys/0190f2c0-77aa-7b21-8c33-1d2e3f405162/rotate-secret \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` La rotació té efecte **a l'instant**: qualsevol petició que segueixi fent servir el secret anterior deixa d'autenticar tan bon punt rotes. Desplega el nou secret abans de — o de manera atòmica amb — la rotació per evitar downtime. És irreversible. ## Revoca una key filla [#revoke] `DELETE /v1/companies/{id}/api-keys/{key}` revoca una key filla de manera permanent. Les peticions posteriors autenticades amb ella deixen de funcionar. Requereix el scope `api_keys:write`. ```bash curl -X DELETE \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys/0190f2c0-77aa-7b21-8c33-1d2e3f405162 \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` La revocació és **irreversible**. Un cop revocada, la key no es pot restaurar — emet-ne una de nova si l'empresa encara necessita accés a l'API. ## Scopes i aïllament [#scopes] Les keys filles es protegeixen amb els scopes d'`api_keys`: * `api_keys:read` — llistar i recuperar keys filles. * `api_keys:write` — crear, rotar i revocar keys filles. Tota operació està acotada al teu tenant mestre. Un `id` d'empresa que pertany a un altre mestre retorna `404` — mai `403`, ni un endpoint de key filla d'una empresa que no gestiones. És el mateix aïllament entre mestres que regeix [les empreses en si](/guides/companies#scopes): un mestre només veu i actua sobre les seves pròpies empreses i les seves keys. --- # Empreses gestionades (/ca/guides/companies) Una **empresa gestionada** és un subcompte fill que crees i operes sota el teu propi tenant mestre. És el model de gestoria: una assessoria (la mestra) manté un únic joc de credencials i, mitjançant elles, dona d'alta i gestiona moltes empreses clients, cadascuna aïllada de les altres. Aprovisiones cada empresa filla i després la manejes de dues maneres: emets una [API key filla](/guides/child-api-keys) acotada a ella, o mantens la teva master key i canvies d'empresa objectiu per petició amb el [header `X-Active-Profile`](/guides/acting-on-behalf). Aquesta pàgina cobreix les empreses en si — crear-les, aprovisionar-les, el seu cicle de vida activa/desactivada, el cobrament de places i l'arxivat. Onze endpoints sota `/v1/companies` gestionen les empreses. | Operació | Endpoint | Scope | | --------------------------------------- | ----------------------------------------- | ------------------ | | Llistar empreses | `GET /v1/companies` | `companies:read` | | Crear una empresa | `POST /v1/companies` | `companies:write` | | Recuperar una empresa | `GET /v1/companies/{id}` | `companies:read` | | Actualitzar una empresa | `PATCH /v1/companies/{id}` | `companies:write` | | Arxivar una empresa | `DELETE /v1/companies/{id}` | `companies:delete` | | Consultar l'estat de creació | `GET /v1/companies/{id}/creation-status` | `companies:read` | | Verificar (reconciliar) la creació | `POST /v1/companies/{id}/verify-creation` | `companies:write` | | Desactivar una empresa | `POST /v1/companies/{id}/deactivate` | `companies:write` | | Reactivar una empresa | `POST /v1/companies/{id}/activate` | `companies:write` | | Activar empreses en bloc | `POST /v1/companies/activate` | `companies:write` | | Previsualitzar el cobrament de la plaça | `GET /v1/companies/seat-charge-preview` | `companies:read` | `{id}` és l'`id` de l'empresa — un UUID v7 opac, **no** el seu `tax_id`. Mira els esquemes complets a la [Referència de l'API](/api-reference/companies/public-api.v1.companies.list). ## Crea una empresa gestionada [#create] `POST /v1/companies` dona d'alta una nova empresa filla sota el teu tenant mestre. `name` i `tax_id` són els únics camps **obligatoris**; la resta del perfil (raó social, adreça fiscal, dades de contacte) és opcional i es pot enviar en la mateixa petició. El `tax_id` (NIF / CIF / NIE) ha de ser **únic entre les empreses que ja gestiones**; un duplicat retorna `409`. Requereix el scope `companies:write`. ```bash 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" }' ``` Resposta (`201`): ```json { "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 } } ``` ### Cos de la petició [#cos-de-la-petició] | Camp | Obligatori | Notes | | --------------- | ---------- | --------------------------------------------------------------------------------- | | `name` | sí | Nom comercial (1–255 caràcters). | | `tax_id` | sí | Identificador fiscal espanyol (NIF / CIF / NIE). **Immutable** després de l'alta. | | `business_name` | no | Raó social, fins a 100 caràcters. | | `address` | no | Adreça fiscal. | | `city` | no | Ciutat de la seu fiscal. | | `postal_code` | no | Codi postal — d'ell es deriva la zona AEAT (`country_aeat_zone` a les respostes). | | `province` | no | Província. | | `country` | no | País. | | `email` | no | Email de contacte. | | `phone` | no | Telèfon de contacte. | El camp `status` de la resposta és el [cicle de vida activa/desactivada](#lifecycle) de l'empresa, un eix diferent del [`provisioning_status`](#provisioning) que segueix l'aprovisionament asíncron. No hi ha camp d'entrada `country_aeat_zone` — envies un `country` de text lliure, i la zona AEAT es deriva del `postal_code`. A l'alta només s'aplica validació a nivell de formulari. La comprovació censal amb l'AEAT és un pas d'aprovisionament a part — donar d'alta una empresa aquí no la verifica contra el cens en la mateixa petició. ### Identificador fiscal duplicat [#identificador-fiscal-duplicat] Reutilitzar un `tax_id` que ja gestiones retorna `409` amb `resource_already_exists`, i `existing_resource_id` apunta a l'empresa que ja el té: ```json { "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" } } ``` El `tax_id` és únic **per tenant mestre**, no a nivell global: dues gestories diferents poden gestionar cadascuna una empresa amb el mateix identificador fiscal. ## Cicle de vida de l'aprovisionament [#provisioning] Donar d'alta una empresa de facturació real **no és instantani**. `POST /v1/companies` respon a l'instant, però al darrere l'empresa filla s'**aprovisiona** de manera asíncrona: es creen una sèrie de documents per defecte i la config fiscal mínima i — en `live` — es cobra a la gestoria la nova plaça. Fins que això acaba, la filla encara no és operativa. Dos endpoints exposen el cicle: un per **consultar-lo** i un altre per **reconciliar-lo**. | Operació | Endpoint | Scope | | ---------------------------------- | ----------------------------------------- | ----------------- | | Consultar l'estat de creació | `GET /v1/companies/{id}/creation-status` | `companies:read` | | Verificar (reconciliar) la creació | `POST /v1/companies/{id}/verify-creation` | `companies:write` | ### Estats d'aprovisionament [#estats-daprovisionament] `provisioning_status` recorre un cicle de vida petit i d'un sol sentit: | Estat | Significat | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | La filla es va donar d'alta; l'aprovisionament encara no ha començat. | | `awaiting_payment` | La gestoria no té cap mètode de pagament registrat, així que la plaça encara no es pot cobrar. `payment_setup_url` apunta a on el tenant mestre n'afegeix un. | | `provisioning` | La plaça es va cobrar (o la filla està en mode de prova) i el tenant s'està configurant. | | `active` | L'aprovisionament ha acabat. La filla és plenament operativa. | | `failed` | L'aprovisionament no s'ha pogut completar — `failed_reason` indica el motiu. Torna a crear l'empresa per reintentar-ho. | Amb una clau de **prova** (`fact_test_`) no hi ha cap crida a Stripe: la filla passa directament a `active`, de manera determinista. El camí d'`awaiting_payment` i el cobrament per-seat només apliquen a les claus `live`. ### Cobrament per-seat [#per-seat] En `live`, cada empresa filla **activa** és una **plaça** que es cobra dins la subscripció ja existent del tenant mestre — una sola factura recurrent que es llegeix com a "pla + N clients". Afegir una filla afegeix una plaça i **cobra el prorrateig de seguida** pel que queda del període de facturació; arxivar una filla treu la plaça i **abona** el temps no usat a la factura següent. La filla no arriba a `active` fins que aquest cobrament immediat té èxit; si falla, la filla acaba en `failed`. Previsualitza l'import per endavant amb [el preview de la plaça](#seat-charge-preview). Com que la plaça viu en la mateixa subscripció del tenant mestre, la filla hereta el pla i els add-ons del mestre, i un impagament de la subscripció del mestre suspèn el compte sencer de la gestoria — les seves filles incloses. No hi ha cap factura separada per filla. ### Consulta l'estat de creació [#creation-status] `GET /v1/companies/{id}/creation-status` retorna el `provisioning_status` actual i les marques de temps. Consulta'l després de crear una empresa fins que arribi a `active` (o `failed`). Requereix el scope `companies:read`. ```bash curl https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/creation-status \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Resposta (`200`): ```json { "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` és present **només** mentre `awaiting_payment`, i `failed_reason` **només** quan `failed`; tots dos són `null` en els altres casos. ### Verifica la creació [#verify-creation] `POST /v1/companies/{id}/verify-creation` reconcilia una filla contra la subscripció del tenant mestre i la fa avançar quan pot. **No porta cos de petició** i és **idempotent**. Requereix el scope `companies:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/verify-creation \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Crida'l un cop el tenant mestre hagi afegit un mètode de pagament, o quan vulguis treure una filla d'`awaiting_payment`: * Una filla que ja està `active` és una **operació sense efecte** — la crida es pot repetir sense risc. * Mentre `awaiting_payment`, si el mestre ja té un mètode de pagament, es cobra la plaça prorratejada i la filla passa a `active`. * Si el mestre encara no té mètode de pagament, la crida no té efecte ni **error** — la filla es manté en `awaiting_payment`. Retorna el mateix recurs de creation-status que l'endpoint de consulta, així que pots llegir el `provisioning_status` resultant directament de la resposta. ## El cicle de vida activa/desactivada [#lifecycle] A part de l'aprovisionament, cada filla porta un `status` — el seu **cicle de vida del vincle** dins la gestoria. És el camp `status` del recurs d'empresa, i recorre tres estats: | Estat | Significat | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | La filla està vinculada i operativa. | | `inactive` | La filla està desactivada — inaccessible fins que la reactives (pagant de nou la seva plaça), però amb les seves dades intactes i de manera reversible. | | `archived` | La filla ha estat desvinculada. És un estat terminal. | Les transicions són `active ↔ inactive` (desactivar / reactivar) i `active → archived` o `inactive → archived` ([arxivar](#archive)). `archived` és terminal. Desactivar allibera la plaça; reactivar la cobra de nou. Això permet a una gestoria aparcar un client entre encàrrecs sense perdre el seu historial, i recuperar-lo més tard. ### Desactiva una empresa [#deactivate] `POST /v1/companies/{id}/deactivate` passa una filla `active` a `inactive`. L'empresa queda inaccessible però conserva totes les seves dades, de manera reversible. **No cobra**: l'abonament prorratejat de la plaça alliberada s'aplica best-effort a la factura següent. Requereix el scope `companies:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/deactivate \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Retorna el recurs d'empresa actualitzat amb `status: "inactive"`. ### Reactiva una empresa [#activate] `POST /v1/companies/{id}/activate` torna una filla `inactive` a `active`. La reactivació està gatejada per un **cobrament atòmic de la plaça**: primer es cobra el prorrateig, i només si el cobrament té èxit la filla passa a `active`. Si el mestre no té mètode de pagament, o el cobrament falla, l'empresa segueix `inactive`. Requereix el scope `companies:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/activate \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Retorna el recurs d'empresa actualitzat amb `status: "active"`. ### Activa empreses en bloc [#activate-batch] `POST /v1/companies/activate` reactiva diverses filles en una sola crida, amb un **únic cobrament conjunt** — una factura per a tot el lot en comptes d'una per empresa. El cos porta `company_ids`, una llista de valors `id` d'empresa filla (1–1000). Requereix el scope `companies:write`. ```bash 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" ] }' ``` El cobrament és atòmic **a nivell de lot** — o totes o cap. La propietat (`404` per a una empresa fora del teu arbre) i la precondició `inactive` (`422`) es validen per a cada empresa **abans** que s'executi cap cobrament. La resposta és la llista d'empreses reactivades (`{ "data": [ … ] }`). ## Previsualitza el cobrament de la plaça [#seat-charge-preview] `GET /v1/companies/seat-charge-preview` retorna el que **costaria** afegir o reactivar empreses filles, sense cobrar res. Fes-lo servir per mostrar el prorrateig abans d'un `POST /v1/companies` o d'una activació, i per detectar per endavant el cas "sense mètode de pagament". Requereix el scope `companies:read`. Té dos modes: * `count` (≥1, default 1) — previsualitza el prorrateig conjunt d'activar aquest nombre de filles en un lot. * `company_ids` — una llista de valors `id` de filles concretes, per a un preview conscient de la cobertura: l'import és `0` amb `already_covered: true` quan totes segueixen cobertes aquest període, i en cas contrari prorrateja només les no cobertes. Quan s'envia, mana sobre `count`. ```bash curl "https://api.factuarea.com/v1/companies/seat-charge-preview?count=1" \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Resposta (`200`): ```json { "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` és la **base imposable** del prorrateig en unitats mínimes de la moneda (cèntims), `tax_amount` l'IVA, i `total` (`amount + tax_amount`) el que es cobra realment. `tax_rate` és el percentatge d'IVA derivat (p. ex. `21`) o `null` si Stripe Tax no el va calcular. Els camps `recurring_*` projecten la quota mensual conjunta després de l'activació: nombre total de places, base sense IVA i total amb IVA (`recurring_total_cents` és `null` quan l'IVA no és calculable). Quatre flags mútuament excloents expliquen un import `0`, per ordre de prioritat: | Flag | L'`amount` és `0` perquè… | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `already_covered` | Les empreses que activaries ja estan incloses en la subscripció d'aquest període — reactivar-les és gratis. | | `requires_active_plan` | La gestoria no té pla vigent i ha de contractar-ne un abans de gestionar empreses. | | `included_in_trial` | La gestoria està en el seu període de prova — l'empresa es crea gratis (les places comencen a cobrar-se quan el trial es converteix en pla de pagament). | | `requires_payment_method` | La gestoria té un pla de pagament però cap mètode de pagament registrat, i ha d'afegir-ne un (Billing Portal) primer. | `is_first_seat` és `true` quan l'activació crea la **primera** subscripció de places del mestre: el càrrec és un mes complet i avui ancora el dia de cobrament mensual del cicle conjunt. ## Llista i recupera empreses [#list] `GET /v1/companies` retorna les teves empreses gestionades amb [paginació per cursor](/guides/pagination); `GET /v1/companies/{id}` en retorna una. Ambdues estan acotades al teu tenant mestre. ```bash curl https://api.factuarea.com/v1/companies?limit=25 \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Una empresa gestionada per un tenant mestre **diferent** retorna `404`, mai `403` — l'API no revela mai que existeix una empresa que no pots gestionar. ## Actualitza una empresa [#update] `PATCH /v1/companies/{id}` és una **actualització parcial**. L'únic camp editable és `name` — el perfil (raó social, adreça fiscal, contacte, zona AEAT) no és editable aquí, i el `tax_id` és **immutable** i es rebutja si l'inclous al cos. Una empresa ha d'estar `active` per editar-se. Requereix el scope `companies:write`. ```bash 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" }' ``` ## Arxiva una empresa [#archive] `DELETE /v1/companies/{id}` **arxiva** l'empresa en lloc d'esborrar-la: el seu `status` passa a `archived` i deixa d'acceptar operacions. Una empresa `active` o `inactive` es pot arxivar; `archived` és terminal. Requereix el scope `companies:delete`. ```bash curl -X DELETE \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` L'arxivat pot quedar **bloquejat**: si l'empresa encara té estat que ho impedeix (per exemple documents pendents), la petició retorna `422` i l'empresa conserva el seu estat actual. Resol abans la condició que ho bloqueja i després arxiva-la. ## Scopes i aïllament [#scopes] Les empreses es protegeixen amb els seus propis scopes: * `companies:read` — llistar i recuperar empreses gestionades, consultar l'estat de creació i previsualitzar el cobrament de la plaça. * `companies:write` — crear, actualitzar, activar i desactivar empreses gestionades, i verificar la seva creació. * `companies:delete` — arxivar empreses gestionades. Tota operació està acotada al teu tenant mestre. Un `id` d'empresa que pertany a un altre mestre retorna `404` — mai `403`. Aquest aïllament entre mestres és la garantia central del model de gestoria: un mestre només veu i actua sobre les seves pròpies empreses. La mateixa garantia regeix [actuar en nom d'una filla](/guides/acting-on-behalf) i les seves [API keys](/guides/child-api-keys). --- # Factures rectificatives (/ca/guides/corrective-invoices) Una factura rectificativa és un document fiscal per dret propi: rep el seu propi número, la seva pròpia alta davant l'AEAT i el seu propi efecte a la declaració d'IVA. Emetre-la implica quatre decisions independents, i les integracions tendeixen a barrejar-les: 1. **Quina factura** es pot rectificar. 2. **Quin codi de rectificació** porta — el motiu legal. 3. **Substitució o diferències** — si la rectificativa declara els imports correctes o només el delta. 4. **Amb quines línies** acaba la rectificativa. Equivoca't alhora a la tercera i a la quarta i presentaràs una declaració d'IVA amb el signe invertit. ## Quan aplica [#when] La factura original ha d'estar `sent` o `paid`. Cap altra no serveix ([`BR-INV-001`](#traceability), RD 1619/2012 art. 15): | Estat de l'original | Rectificable? | | ----------------------- | ----------------------------------------------------------- | | `sent`, `paid` | Sí. | | `draft`, `cancelled` | No — edita'l o elimina'l, encara no és un document fiscal. | | `overdue` | No. Registra abans el cobrament o anul·la-la. | | `annulled` | No — ja s'ha retirat. | | Ja és una rectificativa | No. Emet una rectificativa nova **de la factura original**. | Per a una factura pagada això no és una opció entre diverses: és l'única. Una factura pagada no es pot anul·lar, perquè el seu IVA repercutit ja està compromès amb un període ([`BR-INV-023`](#traceability)). Vegeu [Anul·lar o rectificar](/guides/annul-vs-correct). ## La matriu de codis de rectificació és legal, no cosmètica [#r-codes] El codi de rectificació declara *per què* es rectifica l'original, i l'AEAT restringeix quins codis són legals per a cada tipus d'original. | Tipus de la factura original | Codis legals | | -------------------------------- | ------------------- | | Simplificada `F2` | **només `R5`** | | Completa `F1`, substitutiva `F3` | **només `R1`–`R4`** | Força un codi fora de la seva fila i l'API respon `422` amb els valors legals a `allowed_values` ([`BR-INV-035`](#traceability)). Hi ha dues maneres d'arribar al codi. Per defecte es **deriva** del slug `correction_reason` que envies ([`BR-INV-018`](#traceability)): | `correction_reason` | Codi | Base legal | | -------------------------------------------------------------------- | ---- | ------------------------------------------------------- | | `error_fundado` | `R1` | Art. 80.Uno, Dos y Seis LIVA — error fonamentat de dret | | `concurso` | `R2` | Art. 80.Tres LIVA — concurs de creditors | | `incobrable` | `R3` | Art. 80.Cuatro LIVA — crèdit incobrable | | `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras` | `R4` | RD 1619/2012 art. 15 — resta de causes | `R5` no es deriva mai d'un motiu. Ve del *tipus* de l'original: una rectificativa d'una `F2` neix sempre `R5`, sigui quin sigui el motiu que passis ([`BR-INV-019`](#traceability)). Com a alternativa, fixes `correction_code` de manera explícita. Sobre un original complet, un `R1`–`R4` explícit **guanya** a la derivació per slug i passa a ser el codi que viatja a la cadena VeriFactu. Fes-lo servir quan el teu propi sistema ja conegui la causa legal i no vulguis que s'infereixi d'un slug. `R2` i `R3` exigeixen documentació acreditativa per llei. Passa `justification` (de 10 a 1000 caràcters); s'anteposa a les notes de la rectificativa com a traçabilitat documental. ## Substitució o diferències [#nature] És la decisió de més radi d'impacte, i al contracte v1 no la fixes directament — fixes `correction_type` i la naturalesa se'n deriva: | `correction_type` | Naturalesa | La rectificativa conté | Signe | | ----------------- | --------------------- | ------------------------------------------------------------------------- | ---------------------- | | `full` | `S` — substitució | Els **imports correctes, complets**. Reemplaça l'original sencer. | Sempre positiu o zero. | | `partial` | `I` — per diferències | Només la **diferència** entre el que es va facturar i el que és correcte. | Pot ser negatiu. | La regla que imposa el motor fiscal: una base imposable pot ser negativa **només** en una rectificativa per diferències. En una substitució —i en qualsevol factura ordinària— una base negativa és una dada incoherent i es rebutja amb `422` ([`BR-VFC-033`](#traceability), [`BR-INV-017`](#traceability)). Aquest és el mecanisme per a una correcció a la baixa. Un abonament és una rectificativa per diferències amb base i quota d'IVA negatives, i l'AEAT l'accepta precisament perquè és la manera fiscalment correcta d'expressar un crèdit. Intentar expressar aquest mateix abonament com una substitució amb imports negatius es rebutja. Una rectificativa per diferències sobre una factura al 0 % d'IVA té **base negativa i quota zero** — no quota negativa. El motor fiscal hereta el tipus de les línies de l'original i no se'l inventa mai; fabricar aquí un 21 % és la manera clàssica de collir un rebuig de l'AEAT. ## Què envia l'API [#api] [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective), scope `invoices:write`. Respon `201` amb la factura **nova** i una capçalera `Location` que hi apunta. | Camp | Obligatori | Notes | | ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `correction_reason` | Sí | Un dels vuit slugs de dalt. | | `correction_type` | Sí | `full` o `partial`. | | `correction_code` | No | `R1`–`R5`. Es valida contra la matriu legal. | | `justification` | No | De 10 a 1000 caràcters. A la pràctica, obligatòria per a `R2` i `R3`. | | `notes` | No | Text lliure, fins a 1000 caràcters. | | `lines` | Obligatori quan `correction_type` és `partial` | `description`, `quantity`, `unit_price` i, opcionalment, `tax_rate`, `discount_percent`, `indirect_tax_regime`, `product_id`. | L'API Reference publica un exemple llest per enviar per cada codi — `r1_error_fundado`, `r2_concurso`, `r3_incobrable`, `r4_otras` i `r5_simplificada` — al desplegable d'exemples del cos de petició d'aquesta operació. Es publiquen a més com a entrades reutilitzables `components.examples.corrective_*` del document OpenAPI, de manera que els clients generats les puguin resoldre per `$ref`. ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/corrective \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "correction_reason": "otras", "correction_type": "partial", "correction_code": "R3", "justification": "Crédito declarado incobrable por resolución judicial firme.", "lines": [ { "description": "Ajuste por impago", "quantity": -1, "unit_price": 100, "tax_rate": 21 } ] }' ``` La resposta és un objecte factura ordinari amb `is_corrective` a `true` i amb un bloc `corrective` que porta `original_id`, `original_number`, `original_date`, `correction_reason`, `correction_type`, `correction_nature`, `base_rectificada`, `cuota_rectificada` i `correction_aeat_type` — aquest últim és el codi de rectificació que va viatjar de debò a l'AEAT. Per llistar totes les rectificatives emeses contra una factura, fes servir [`GET /v1/invoices/{id}/correctives`](/api-reference/invoices/public-api.v1.invoices.correctives). ### Com es construeixen les línies [#lines] Les tres combinacions produeixen documents genuïnament diferents ([`BR-INV-036`](#traceability)): **`full` sense `lines`** — una anul·lació completa. Es genera una línia per cada línia de l'original amb la quantitat **negada**, conservant el producte, el preu, el tipus impositiu, la retenció, el recàrrec, el descompte i el règim indirecte de l'original. **`full` amb `lines`** — una substitució. Les línies que envies **són** les línies finals; no hi ha comparació de diferències. Cada camp que ometis s'hereta **per índex** de la línia original equivalent, fiscalitat inclosa. L'herència no cau mai a un tipus per defecte, així que una operació exempta continua exempta en lloc d'adquirir un 21 % fantasma. Si envies més línies de les que tenia l'original, les sobrants no tenen contrapart: no porten producte i la seva fiscalitat queda a zero. **`partial`** — línies d'ajust. No hi ha cap línia original amb què casar per índex, així que els valors per defecte són zero i `product_id` viatja només si el declares explícitament. Una línia sense `product_id` no mou inventari. L'herència per índex dona per fet que les línies rectificades arriben **en el mateix ordre** que les originals. Reordenar o suprimir línies creua els valors heretats. Quan qui et crida reordeni, declara els camps de manera explícita a cada línia en lloc de recolzar-te en l'herència. Les línies de suplert també s'hereten de l'original, i per això el payload de la rectificativa accepta `line_type` i `source_invoice_reference` en una línia. Vegeu [Suplerts](/guides/disbursements). ## Què surt al PDF [#pdf] La rectificativa s'imprimeix com a document a part amb el seu propi número, derivat de l'original: `SERIE-AAAA-NNN-REC{n}`, on `{n}` compta les rectificatives ja emeses contra aquest original ([`BR-INV-021`](#traceability)). Els seus blocs de destinatari i emissor es congelen **en el seu propi moment d'emissió**, no es copien de l'original. És deliberat: un motiu habitual per rectificar és precisament que les dades del destinatari estaven malament, així que la rectificativa ha d'imprimir les corregides ([`BR-INV-024`](#traceability)). Com qualsevol factura emesa per una empresa adherida a VeriFactu, porta el bloc QR legal. ## Què arriba a l'AEAT [#aeat] **Com a registre VeriFactu**, la rectificativa és una alta ordinària amb un `invoice_type` que és el codi de rectificació. El seu desglossament fiscal porta el signe descrit a [Substitució o diferències](#nature): base i quota negatives per a una correcció a la baixa per diferències, sempre no negatives per a una substitució. La substitució declara a més la base i la quota rectificades de l'original; una correcció per diferències no ho fa, en línia amb l'esquema de l'AEAT ([`BR-VFC-033`](#traceability)). **A la declaració trimestral d'IVA**, la rectificació d'una operació en règim general aterra a les caselles `[14]` i `[15]` **amb el seu signe**: una correcció a la baixa resta, una a l'alça suma. El *snapshot* fiscal conserva el codi real (`R1`–`R4`) en lloc de col·lapsar tota rectificativa a `R5` ([`BR-TXR-019`](#traceability)). Aquest encaminament aplica només al règim general. Una rectificativa amb un règim d'operació de capçalera diferent segueix les caselles pròpies d'aquell règim — la inversió del subjecte passiu i les operacions exemptes o d'exportació es declaren en un altre lloc i per tant **no** arriben a `[14]`/`[15]`. Vegeu [Claus de règim](/guides/regime-keys) per saber com es determina el règim de capçalera. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-INV-001` — una rectificativa ha de referenciar un original emès; els estats admissibles. * `BR-INV-017` — la naturalesa de la rectificació és exactament `S` o `I`. * `BR-INV-018` — el mapatge de slug de motiu a `R1`–`R4`. * `BR-INV-019` — una rectificativa d'una `F2` neix `R5`. * `BR-INV-021` — la numeració `-REC{n}` de les rectificatives. * `BR-INV-024` — el *snapshot* immutable del destinatari, congelat en el moment d'emissió de la rectificativa mateixa. * `BR-INV-035` — `correction_code` explícit, la matriu legal de l'AEAT i `justification`. * `BR-INV-036` — com es generen les línies de la rectificativa i què s'hereta per índex. * `BR-VFC-033` — base i quota negatives admeses només en rectificacions per diferències. * `BR-TXR-019` — el signe a les caselles `[14]`/`[15]` i la conservació del codi de rectificació real. --- # Suplerts (/ca/guides/disbursements) Un **suplert** (*suplido* en la nomenclatura fiscal espanyola) és una quantitat pagada **en nom i per compte del client**, sota el seu mandat exprés (art. 78.Tres.3 de la Llei de l'IVA). No forma part del que cobres pel teu servei: l'avances, el repercuteixes a cost i no arriba mai a ser la teva base imposable. Facturat com a línia ordinària, aquest mateix import infla la teva base imposable, el teu IVA repercutit, el total que declares a l'AEAT i la base que informes d'aquell client a la declaració anual d'operacions amb tercers (**Modelo 347**). Facturat com a suplert, apareix al document, el client el paga i queda fora de les quatre coses. ## Quan aplica [#when] Només a **factures emeses**. Els pressupostos, les proformes, els albarans, les factures de compra i les plantilles de recurrents no modelen els suplerts en absolut — les seves taules de línies no tenen aquesta columna ([`BR-INV-037`](#traceability)). Una plantilla de recurrent, en particular, no podria portar la referència d'origen obligatòria, així que la línia degradaria en silenci a una operació ordinària i cada factura generada la declararia com a ingrés propi. Dues restriccions més: * **Una factura simplificada no en pot portar cap.** El contingut obligatori d'una factura simplificada no identifica el destinatari, així que no pot acreditar per compte de qui es va pagar l'import, i l'Administració tributària el tractaria com a base imposable teva. La seva rectificativa es rebutja pel mateix motiu. Emet una factura completa o treu la línia ([`BR-INV-040`](#traceability)). * **Una factura no pot estar feta només de suplerts.** S'exigeix almenys una línia ordinària ([`BR-INV-046`](#traceability)). L'API només pot imposar una de les tres condicions legals: que puguis justificar l'import. El **mandat exprés del client** és un requisit documental que Factuarea ni demana ni desa: sense ell l'import no és un suplert, l'etiqueti com l'etiqueti la factura. I **l'IVA suportat d'un suplert no és deduïble per tu** — el destinatari real d'aquella operació és el client. Res del producte no t'impedeix deduir-lo, així que això queda de la teva mà. ## Què envia l'API [#api] Quatre camps opcionals de línia a [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create), [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update) i [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective): | Camp | Regles | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `line_type` | `NORMAL` o `SUPLIDO`. Absent o `null` equival a `NORMAL`, així que ometre'l reprodueix exactament el comportament anterior. | | `source_invoice_reference` | **Obligatori** en una línia de suplert. Text lliure, fins a 100 caràcters. | | `source_invoice_ids` | Traçabilitat opcional: factures de compra **de la teva pròpia empresa**, validades amb abast de tenant. Una llista buida col·lapsa a nul. | | `line_total` | Suma de control opcional d'entrada — vegeu [La suma de control de línia](#checksum). | ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Honorarios de constitución de sociedad", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }, { "description": "Tasa del Registro Mercantil", "quantity": 1, "unit_price": 150, "line_type": "SUPLIDO", "source_invoice_reference": "RM-2026-0451" } ] }' ``` `POST /v1/invoices` no té camp `type`, així que no pot emetre una factura simplificada; el rebuig per factura simplificada només s'assoleix, per tant, a través de l'endpoint de rectificativa sobre un original simplificat. Vegeu [Factures simplificades o completes](/guides/simplified-vs-full-invoices#f2-not-in-v1). ### La referència d'origen és obligatòria, i és text [#reference] És text lliure i no una clau forana perquè el justificant —una taxa judicial, un aranzel registral, un visat— rarament està registrat com a factura de compra a Factuarea. Sense ell no pots acreditar que la despesa pertany al client ([`BR-INV-038`](#traceability)). `source_invoice_ids` és la contrapart estructurada opcional, i la regla pràctica convé interioritzar-la: > **Si el justificant està al teu nom, no és un suplert.** > Factura'l com a línia ordinària. El suplert canònic té el document expedit a nom del **client**, així que no és una compra teva i la llista es queda buida. Enllaça factures de compra només quan hagis registrat de debò el pagament als teus propis llibres com a suport de la bestreta — i recorda que l'IVA suportat d'aquella factura no s'ha de deduir. ### Una línia de suplert no porta càrrega fiscal pròpia [#no-tax] Vuit camps es rebutgen en una línia `SUPLIDO` amb un valor diferent de zero o de nul ([`BR-INV-039`](#traceability)): | Camp | Per què | | ------------------ | ---------------------------------------------------------------------------------------------- | | `tax_rate` | Un suplert no és contraprestació — no li repercuteixes IVA. | | `retention_rate` | No hi ha cap ingrés teu sobre el qual retenir. | | `surcharge_rate` | El recàrrec d'equivalència grava un lliurament teu; això no ho és. | | `discount_percent` | Descomptar un import pagat per compte d'altri el distorsiona — repercuteixes el que vas pagar. | | `regime_key` | Una clau de règim qualifica una operació teva. | | `exemption_reason` | Un suplert ni tributa ni està exempt: no és operació teva. | | `product_id` | No és un lliurament de béns teus i no ha de moure estoc. | | `pack_id` | Mateix motiu — un pack s'expandeix en lliuraments propis. | L'error anomena el camp infractor, i el porta com a `offending_field` al detall de l'error. Com que una línia de suplert no pot referenciar cap producte, el llibre d'estoc la ignora **per construcció**: la fila persistida no té producte i ja queda filtrada. ### La suma de control de línia [#checksum] `lines[].line_total` és una suma de control **d'entrada i opcional**. Quan hi és, es compara amb el total que el motor acaba de calcular, i la petició es rebutja si la desviació supera un cèntim ([`BR-INV-044`](#traceability)). El detall de l'error porta els valors `expected` i `received` perquè localitzis un desquadrament d'arrodoniment amb el teu ERP sense haver de parsejar el missatge. Tres propietats, totes deliberades: * **No es persisteix mai, no es retorna mai.** No existeix aquesta columna i cap recurs no l'emet. L'import facturat és sempre el que calcula Factuarea. * **No és mai obligatòria**, en cap escenari. Exigir-la t'obligaria a replicar el nostre motor de càlcul, cosa explícitament fora d'abast. * **La tolerància d'un cèntim és inclusiva.** Una desviació d'exactament 0,01 € passa; 0,02 € falla. La comparació es fa en aritmètica de precisió arbitrària, no en coma flotant — l'error de coma flotant és precisament el que aquest camp existeix per diagnosticar. ### Errors [#errors] Tots `422`: | `subcode` | Causa | | ------------------------------------------- | --------------------------------------------------------------------------- | | `suplido_requires_source_invoice_reference` | La línia de suplert no té referència d'origen. | | `suplido_line_cannot_carry_taxes` | S'ha enviat un dels vuit camps prohibits. | | `suplido_not_allowed_in_simplified_invoice` | Una factura simplificada o la seva rectificativa. | | `invoice_requires_at_least_one_line` | Totes les línies són suplerts, així que la factura no declara cap operació. | | `line_total_checksum_mismatch` | El total de línia declarat es desvia més d'un cèntim. | L'índex del missatge comença a zero sobre la col·lecció completa de línies, de manera que casa amb la ruta `lines.{i}` del teu payload. ## Com queden els totals [#totals] La calculadora de totals particiona les línies per tipus ([`BR-INV-041`](#traceability)): | Camp | Contingut | | ---------------------------------- | -------------------------------------------------------------- | | `subtotal`, `taxes_total`, `total` | Només les línies ordinàries. La fórmula queda intacta. | | `total_disbursements` | La suma de les línies de suplert, i **només** això. Persistit. | | `total_to_pay` | `total + total_disbursements`. **Derivat**, mai emmagatzemat. | Per a la factura de dalt: subtotal 1000, IVA 210, total 1210, suplerts 150, import a pagar 1360. Hi ha exactament un punt del codi on se sumen aquests dos termes, i tots els consumidors —recursos de l'API, el PDF, l'enllaç públic del document— llegeixen el valor derivat en lloc de recompondre la suma. Dues columnes anomenades «total» acabarien divergint. **Tota xifra per factura que mesura deute fa servir l'import a pagar, no el total fiscal** ([`BR-INV-045`](#traceability)): `pending_amount` és `total_to_pay − paid_amount`, el llibre de cobraments accepta un pagament que cobreixi l'import a pagar íntegre sense respondre «supera el pendent», la transició a `paid` exigeix l'import a pagar cobert —pagar només el total fiscal deixa la factura sense cobrar amb el suplert pendent— i els tres enllaços de pagament en línia cobren l'import a pagar. Les xifres **agregades** de cartera són l'excepció documentada: mesuren volum facturat, no import degut. Aquest límit, i el que afecta els documents Facturae i UBL, són a [Abast i limitacions](/guides/scope-and-limitations#gaps). Una factura sense suplerts té `total_disbursements: 0` i `total_to_pay == total`, al cèntim, inclosa tota factura històrica. ## Què surt al PDF [#pdf] El suplert **sí** que s'imprimeix —el client el va pagar i la factura és la representació legal d'això— però marcat com el que és ([`BR-INV-042`](#traceability)): la línia mostra un guionet a la columna d'IVA, i el bloc de totals guanya una fila *Suplidos* i una fila *Total a pagar* a sota del total fiscal. L'enllaç públic del document mostra el mateix. L'exportació a full de càlcul a nivell de línia hi afegeix una columna de tipus de línia, perquè sense ella un suplert és **indistingible d'una operació al 0 % d'IVA** i sumar la columna de total de línia donaria l'import cobrat en lloc de l'ingrés declarable. Dos camps de línia que són només de presentació ajuden aquí i no tenen cap efecte fiscal ([`BR-INV-043`](#traceability)): `unit`, una unitat de mesura de text lliure impresa al costat de la quantitat, i `exemption_reason_text`, text lliure imprès sota la descripció per a la redacció de l'exempció quan la causa catalogada no la cobreix. ## Què arriba a l'AEAT [#aeat] **Res.** Una línia de suplert no arriba mai al registre de facturació VeriFactu: ni al desglossament fiscal, ni al total declarat ([`BR-VFC-036`](#traceability)). L'exclusió passa en un **únic punt**, la passarel·la de lectura, aigües amunt del constructor del desglossament — així el mateix conjunt filtrat alimenta tots els consumidors: l'array de línies, el tipus d'IVA agregat, la descripció de l'operació, la clau de règim i el generador d'XML. Filtrar només l'array de línies hauria deixat oberts els altres camins: un suplert en primera posició donava un tipus del 0 % a l'agregat d'una factura que sí que repercuteix IVA, i descrivia l'operació a l'AEAT com a «Tasa del Registro…». El total declarat no canvia de fórmula i exclou els suplerts per construcció, perquè el total fiscal agrega només les línies ordinàries. L'AEAT valida aquest total contra la suma del desglossament; afegir-hi el suplert desquadraria el registre i en provocaria el rebuig. L'import a pagar és presentació i **no es transmet mai**. **A la declaració anual d'operacions amb terceres persones**, la base declarada de cada contrapart és ([`BR-TXR-023`](#traceability)): ``` base = total facturat (IVA inclòs) + retenció d'IRPF − suplerts ``` La retenció **suma** —la contrapart va rebre una factura per l'import brut— i el suplert **resta**, perquè només el vas repercutir per compte del teu client. Invertir qualsevol dels dos signes declara malament la contrapart. Mentre el terme de suplerts va ser un zero fixat de manera rígida, la declaració **sobredeclarava** tot client a qui s'haguessin repercutit taxes o aranzels, amb risc de desquadrament contra la seva pròpia declaració creuada. Les factures de compra no modelen ni retenció ni suplerts, així que tots dos termes són estructuralment zero al costat rebut. Que un tercer es declari o no es decideix **al contacte**, no a la factura. El camp `accumulate_347` del client —escrivible des de la v1 a [`POST /v1/clients`](/api-reference/clients/public-api.v1.clients.create) i [`PUT /v1/clients/{id}`](/api-reference/clients/public-api.v1.clients.update), amb valor per defecte `true`— exclou totes les operacions d'aquell client quan val `false`, i es llegeix en viu en calcular la declaració en lloc de congelar-se en emetre ([`BR-TXR-037`](#traceability)). L'antiga marca per factura sobreviu com a **override adormit**, exposada en només lectura a l'objecte factura de la v1 com a `exclude_347`: pot forçar l'exclusió d'una factura concreta, mai reincloure un tercer ja marcat com a no acumulable, i l'API pública no la fixa ([`BR-TXR-024`](#traceability)). Cap de les dues marques no reinclou el que les regles automàtiques ja van excloure —operacions intracomunitàries, exportacions i factures simplificades sense NIF—. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-INV-037` — el catàleg tancat de tipus de línia `NORMAL|SUPLIDO`, el seu valor per defecte retrocompatible i per què existeix només a les factures emeses. * `BR-INV-038` — la referència d'origen obligatòria, la traçabilitat opcional a factures de compra i les dues condicions legals que el programari no pot imposar. * `BR-INV-039` — els vuit camps que una línia de suplert no pot portar. * `BR-INV-040` — sense suplerts en una factura simplificada ni en la seva rectificativa. * `BR-INV-041` — suplerts fora de la base, de l'IVA i del total; l'agregat persistit i la fórmula única derivada de l'import a pagar. * `BR-INV-042` — quines superfícies exclouen el suplert i quines el mostren marcat. * `BR-INV-043` — `unit` i `exemption_reason_text` com a camps només de presentació. * `BR-INV-044` — `line_total` com a suma de control d'entrada, opcional, mai persistida, amb tolerància inclusiva d'un cèntim. * `BR-INV-045` — el saldo pendent mesurat contra l'import a pagar. * `BR-INV-046` — una factura no es pot compondre només de suplerts. * `BR-VFC-036` — els suplerts no arriben mai al registre de facturació, i la invariant de *huella* idèntica a les factures que no en porten. * `BR-TXR-023` — la base de la declaració d'operacions amb tercers: total facturat més retenció menys suplerts. * `BR-TXR-037` — l'acumulació en aquella declaració es decideix al contacte, es llegeix en viu, i la marca per factura queda com a override adormit. * `BR-TXR-024` — la marca d'exclusió per document, superseded per `BR-TXR-037` i conservada com aquell override. --- # Facturació de places d'empleat (/ca/guides/employee-seats) Els empleats es facturen mitjançant un **add-on per plaça**, no pel límit `users` del pla — un empleat **mai** computa contra aquest límit. L'add-on és una **subscripció mensual dedicada** (`employee-seats`), totalment separada de la subscripció del pla: el seu `quantity` segueix el nombre d'**empleats actius**, i contractar-lo activa el mòdul `control_horario`. Tots els endpoints viuen sota `https://api.factuarea.com/v1` i usen `employees:read` (estat, preview) o `employees:write` (contractar, canviar quantitat, cancel·lar). ## Com es facturen les places [#model] Una **plaça pagada cobreix tot el període** de facturació. El nombre de places segueix la teva plantilla activa de manera automàtica: * **Activar o donar d'alta** un empleat la plaça del qual no està coberta cobra una plaça prorratejada pel que resta del període. * **Donar de baixa** un empleat allibera la plaça **sense crèdit** (el període ja està pagat) però conserva la seva cobertura, així que **reactivar-lo** dins del mateix període és **gratis**. * Cada **renovació del període** refresca la cobertura dels empleats actius en aquell moment. La quantitat es manté sincronitzada amb el nombre real d'actius mitjançant esdeveniments de l'empleat i una reconciliació horària, així que rarament necessites fixar-la a mà. Per a un compte enterprise facturat **per contracte** (sense subscripció Stripe), l'add-on es concedeix gratis: sense cobrament, sense mètode de pagament exigit, i el mòdul `control_horario` s'habilita igualment. Cancel·lar retira el mòdul de seguida. ## Consultar l'estat de facturació [#status] `GET /v1/employee-seats` retorna l'estat de l'add-on: si la subscripció està activa (`subscribed`), quantes places es facturen (`quantity`), quants empleats estan actius, i el cost recurrent per plaça amb IVA inclòs. **Els imports van en cèntims (unitats menors)** i són `null` —mai un `0` enganyós— quan el cost no és resoluble (sense subscriure, sense pla actiu, enterprise fora de Stripe, sandbox). ```bash curl https://api.factuarea.com/v1/employee-seats \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ## Previsualitzar el càrrec [#preview] `GET /v1/employee-seats/preview` retorna l'import per plaça **prorratejat** per activar o donar d'alta, calculat des de la pròxima factura de Stripe, **sense cobrar**. Mai llança error — degrada a un preview neutre. | Paràmetre | Notes | | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `count` | Preview en lot per a N places (≥1, fins a 1000). | | `employee_ids` | Preview conscient de la cobertura per UUID v7: els empleats encara coberts aquest període costen `0` (`already_covered: true`). | `amount` és la base imposable en cèntims; `requires_payment_method` és `true` quan no hi ha mètode de pagament arxivat. ```bash curl -G https://api.factuarea.com/v1/employee-seats/preview \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "count=3" ``` ## Contractar l'add-on [#subscribe] `POST /v1/employee-seats/subscribe` contracta (opt-in): crea la subscripció mensual `employee-seats` amb `quantity` igual als teus empleats actius i cobra el primer període amb el mètode de pagament arxivat. El cobrament és **atòmic** — si no qualla, **no** es contracta res: * Sense mètode de pagament → `402 employee_seat_payment_method_required`; l'embolcall d'error porta `error.details.payment_setup_url` per completar l'alta de la targeta. * Un cobrament rebutjat → `402 employee_seat_charge_failed`. ```bash curl -X POST https://api.factuarea.com/v1/employee-seats/subscribe \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Si contractar retorna `402 employee_seat_payment_method_required`, envia l'usuari al `payment_setup_url` de l'error, deixa que hi afegeixi una targeta i reintenta. No es cobra ni es contracta res fins que el primer període qualla. ## Sincronitzar la quantitat i cancel·lar [#manage] `POST /v1/employee-seats/change-quantity` reconcilia el nombre de places facturades amb el nombre real d'empleats actius (un `SET` sense prorrateig ni factura). És **idempotent** — un no-op quan la quantitat ja coincideix. `POST /v1/employee-seats/cancel` cancel·la l'add-on **a fi de període**: el mes en curs ja està pagat, així que `subscribed` continua `true` fins que el període acaba, i la cobertura per empleat es purga aleshores. La **subscripció del pla mai es toca**. ```bash curl -X POST https://api.factuarea.com/v1/employee-seats/cancel \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Consulta els esquemes a la [Referència d'API](/api-reference/employees/public-api.v1.employee-seats.status). ## Flux típic [#flow] 1. **Previsualitza** el càrrec de les places que activaràs. 2. **Contracta** l'add-on (primer període cobrat de manera atòmica). 3. Afegeix o treu empleats — la **quantitat s'autosincronitza**; reconcilia de manera explícita amb change-quantity si cal. 4. Llegeix l'**estat** per mostrar les places facturades i el cost per plaça. 5. **Cancel·la** a fi de període quan ja no el necessitis. ## Pròxims passos [#next] * [Visió general del control horari](/guides/workforce-overview) — el rol d'empleat només-portal i tot el sistema. * [Empreses gestionades](/guides/companies) — facturació per plaça de les empreses filles de gestoria. --- # Gestió d'errors (/ca/guides/errors) Tota resposta d'error de l'API pública usa un embolcall JSON consistent. L'estat HTTP indica la categoria general; el camp `type` desambigua i el camp `code` apunta a la causa específica. ## Embolcall [#embolcall] ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "El campo client_id es obligatorio.", "param": "client_id", "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA", "doc_url": "https://docs.factuarea.com/guides/errors#parameter_invalid" } } ``` Camps: * `type` — categoria general de l'error. Estable i enumerada (llista a sota). * `code` — causa específica. Estable i enumerada. * `subcode` — opcional. Present quan el `code` per si sol és ambigu: en els conflictes de duplicació `409` assenyala la clau duplicada exacta (p. ex. `subcode: "tax_id_already_exists"`), i en els errors de pagament `402` assenyala quin gate ha rebutjat la crida (p. ex. `subcode: "webhooks_addon_required"`). Com el `code`, és estable i invariant entre idiomes i versions de l'API. * `message` — text per a persones **en castellà**. **No** es garanteix estable entre versions; útil per a logging i visualització. * `param` — opcional, present en errors de validació. Apunta al **primer** camp problemàtic. En errors de validació de diversos camps el conjunt complet és a `errors[]` (vegeu a sota). * `errors[]` — opcional, present en errors de validació `422`. Llista **tots** els camps fallits (vegeu [Errors de validació de diversos camps](#errors-de-validació-de-diversos-camps)). * `details` — opcional. Porta `existing_resource_id` en els conflictes de duplicació `409` (vegeu [Conflictes de duplicació](#conflictes-de-duplicació)) i `payment_setup_url` en els errors `402` que necessiten un mètode de pagament configurat (vegeu [payment\_required\_error](#payment_required_error)). * `doc_url` — opcional. Enllaç a aquesta guia amb àncora al `code` específic (`#{code}`). * `request_id` — identificador únic de la petició (`req_`). Inclou-lo sempre quan contactis amb suport. També es retorna al header de resposta `X-Request-Id`. L'objecte `error` sempre porta `type`, `code` i `message`; la resta de camps són presents quan és rellevant. ## Errors de validació de diversos camps [#errors-de-validació-de-diversos-camps] Un error de validació `422` reporta **tots** els camps fallits, no només el primer. Els `param`/`message` plans continuen reflectint el primer camp (per retrocompatibilitat), i `errors[]` porta un ítem per camp fallit — així corregeixes tots en una sola petició en lloc d'una petició per camp. ```json { "error": { "type": "invalid_request_error", "code": "invalid_param_value", "message": "El campo client_id es obligatorio.", "param": "client_id", "errors": [ { "param": "client_id", "code": "parameter_missing", "message": "El campo client_id es obligatorio." }, { "param": "issue_date", "code": "parameter_invalid_format", "message": "El formato de la fecha no es válido.", "expected_format": "YYYY-MM-DD" }, { "param": "status", "code": "parameter_invalid_enum", "message": "El valor no es válido.", "allowed_values": ["draft", "sent", "paid"] } ], "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA", "doc_url": "https://docs.factuarea.com/guides/errors#invalid_param_value" } } ``` Cada ítem de `errors[]` porta: * `param` — el nom del camp fallit. * `code` — un codi estable i machine-readable derivat de la regla de validació fallida (p. ex. `parameter_missing`, `parameter_invalid_format`, `parameter_invalid_enum`, `parameter_invalid_integer`). * `message` — descripció llegible de l'error del camp. * `expected_format` — opcional. Present només en errors de format; el patró esperat (p. ex. `YYYY-MM-DD`, `uuid`, `email`, `url`). * `allowed_values` — opcional. Present només en errors d'enum; la llista de valors legals. `errors[]` és purament additiu — les integracions que només llegeixen `param`, `code` i `message` continuen funcionant sense canvis. ## Conflictes de duplicació [#conflictes-de-duplicació] Un conflicte de duplicació `409` (`code: resource_already_exists` amb un `subcode` de `tax_id_already_exists`, `external_id_already_exists` o `sku_already_exists`) retorna l'id del recurs preexistent a `details.existing_resource_id`. Resol-lo amb un sol `GET` en lloc d'un `find_by_*` addicional. ```json { "error": { "type": "conflict_error", "code": "resource_already_exists", "subcode": "tax_id_already_exists", "message": "Ya existe un cliente con ese NIF.", "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" }, "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM", "doc_url": "https://docs.factuarea.com/guides/errors#resource_already_exists" } } ``` Un `GET /v1/clients/0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40` retorna el recurs existent (`200`). El mateix aplica en `PUT` quan un `external_id` ja pertany a un altre recurs de l'empresa. ## Detalls del problema — Problem Details (RFC 9457) [#detalls-del-problema--problem-details-rfc-9457] Envia `Accept: application/problem+json` per rebre el mateix error com un document Problem Details de [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) amb `Content-Type: application/problem+json`. Amb `Accept: application/json`, `Accept: */*` o sense header `Accept` obtens l'embolcall pla de dalt. ```json { "type": "https://docs.factuarea.com/errors/resource_already_exists", "title": "Resource already exists", "status": 409, "detail": "Ya existe un cliente con ese NIF.", "instance": "/v1/clients", "code": "resource_already_exists", "subcode": "tax_id_already_exists", "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" }, "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM" } ``` * `type` — la pàgina de documentació d'aquest `code` concret, per exemple `https://docs.factuarea.com/errors/resource_already_exists`. El `code` és l'únic segment variable, així que pots construir i comparar la URI pel teu compte. Abans era una sola URI compartida per tots els problemes: si hi compares, compara millor contra `code`, que no es mou mai. * `title` — un resum humà breu del tipus de problema. * `status` — el codi d'estat HTTP. * `detail` — el missatge llegible. * `instance` — el path del recurs afectat. La variant problem+json **no descarta** cap dada estesa: `code`, `subcode`, `param`, `errors[]`, `details`, `doc_url` i `request_id` es conserven com a membres d'extensió RFC 9457. ## Missatges localitzats [#missatges-localitzats] El `message` (i el `detail` de problem+json) es localitza via el header `Accept-Language` per als codis del catàleg estable. Els idiomes suportats són `es`, `en` i `ca`, amb fallback a `es` quan el header és absent o demana un idioma no suportat. El `code` i el `subcode` són **invariants** entre idiomes — ramifica sempre per `code`, mai per `message`. ``` Accept-Language: en → missatge en anglès Accept-Language: ca-ES → missatge en català (absent / Accept-Language: de) → missatge en castellà (fallback) ``` Els missatges dinàmics emesos per excepcions de domini queden en castellà; només es localitzen els missatges del catàleg estable. ## Tipus d'error [#tipus-derror] | type | HTTP | Descripció | | --------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_request_error` | `400` o `422` | Payload malformat, paràmetres absents/invàlids o fallada de validació de negoci. | | `authentication_error` | `401` | L'API key falta, és invàlida, està revocada, ha expirat o la IP no és a la llista d'accés. | | `payment_required_error` | `402` | L'operació cobra diners i el pagament no s'ha pogut completar: no hi ha mètode de pagament configurat, el càrrec s'ha denegat, o cal una subscripció o un add-on que no està contractat. | | `authorization_error` | `403` | La key és vàlida però el scope no cobreix l'endpoint. | | `permission_error` | `403` | El pla de l'empresa no dona accés a la funcionalitat. | | `not_found_error` | `404` | El recurs sol·licitat no existeix o no pertany a l'empresa de la key. | | `conflict_error` | `409` | Conflicte de creació, lock d'idempotència o recurs duplicat (p. ex. un `tax_id` ja registrat). | | `idempotency_error` | `409` | Reutilització d'`Idempotency-Key` amb un payload diferent. | | `rate_limit_error` | `429` | Superada la quota per minut o mensual, o massa fallades d'autenticació. | | `api_error` | `500` | Error inesperat del backend. Els reintents poden ajudar; reporta a suport amb el `request_id`. | | `service_unavailable_error` | `503` | API pública deshabilitada via kill-switch, o caiguda d'una dependència (Stripe, mailer). | **`402` i `403` no són intercanviables.** Un `402` (`payment_required_error`) significa que l'operació és al teu abast i que l'única cosa que s'hi interposa són els diners: configura un mètode de pagament, resol el càrrec denegat, o contracta el pla o l'add-on al qual es factura. Un `403` significa que l'accés mateix està denegat —o la key no té el scope (`authorization_error`), o el pla de l'empresa no inclou la funcionalitat (`permission_error`)— i cap reintent de pagament no ho canvia. La parella `addon_required` (`402`, l'add-on no està contractat) i `addon_not_active` (`403`, cap key no arriba a una funcionalitat no contractada) és la que convé llegir dues vegades. Les **violacions de regles de negoci** (transició d'estat invàlida, una acció no permesa en l'estat actual del document) responen `422` amb `type: invalid_request_error` i `code: invalid_status_transition` — **no** `409`. `409 conflict_error` es reserva per a creació duplicada, conflictes d'idempotència i locks de concurrència. ## Catàleg de codes [#catàleg-de-codes] L'àncora de cada encapçalament H3 coincideix exactament amb el valor del camp `code` de l'embolcall. El `doc_url` que retorna l'API resol a la secció específica. La llista de sota cobreix els codes que trobaràs a la pràctica; la referència OpenAPI en viu documenta els codes exactes per endpoint. Per a la referència **completa** de cada error `code` agrupat per bounded context, amb el seu estat HTTP i type, consulta [Tots els error codes](/guides/errors/all). ### invalid\_request\_error [#invalid_request_error] ### parameter\_invalid [#parameter_invalid] Un paràmetre de la petició falta o és invàlid. `param` apunta al camp problemàtic (p. ex. `client_id`, `lines[0].quantity`). ### parameter\_invalid\_format [#parameter_invalid_format] El format d'un valor és incorrecte per a la seva semàntica (regex, longitud, codificació, un UUID malformat, una data fora de format). ### parameter\_invalid\_range [#parameter_invalid_range] Un valor numèric o de data està fora del rang permès (p. ex. `limit` fora d'`1..100`). ### parameter\_invalid\_cursor [#parameter_invalid_cursor] El cursor `starting_after` / `ending_before` no és un `id` de recurs vàlid. Consulta [Paginació](/guides/pagination). ### parameter\_unknown [#parameter_unknown] El body conté un camp no documentat (en endpoints estrictes). ### invalid\_param\_format [#invalid_param_format] Va fallar una restricció de format en un camp tipat — p. ex. el header `Idempotency-Key` o el header `Factuarea-Version` està malformat. ### invalid\_param\_value [#invalid_param_value] El valor no compleix una restricció (enum, format, regla semàntica). ### invalid\_period [#invalid_period] El període de report sol·licitat és invàlid (p. ex. un trimestre/any que no existeix). ### invalid\_status\_transition [#invalid_status_transition] La transició sol·licitada està prohibida per la màquina d'estats del document (p. ex. enviar una factura que no està en un estat enviable). Les violacions de regles de negoci com aquesta són `422`, no `409`. ### invoice\_already\_paid [#invoice_already_paid] `mark-paid` sobre una factura ja pagada. ### quote\_already\_accepted [#quote_already_accepted] Acció que entra en conflicte amb un pressupost ja acceptat. ### business\_rule\_violation [#business_rule_violation] Una invariant de domini va bloquejar l'operació. El `subcode` identifica la regla i `param` el camp infractor. El fa servir el ledger de pagaments ([Registrar pagaments](/ca/guides/payments)): * `payment_exceeds_pending_amount` (`param: "amount"`) — l'import del pagament és més gran que el saldo pendent de la factura. S'aplica tant a `POST /v1/invoices/{id}/payments` com a `POST /v1/purchase_invoices/{id}/payments`. * `invalid_payment_date` (`param: "paid_on"`) — la data de pagament cau fora de la finestra permesa `data_emissió … avui` (factures de compra). * `purchase_invoice_not_payable` (`param: "status"`) — la factura de compra està cancel·lada i ja no admet pagaments. ```json { "error": { "type": "invalid_request_error", "code": "business_rule_violation", "subcode": "payment_exceeds_pending_amount", "message": "El importe del pago (1.500,00 €) supera el importe pendiente de la factura (710,00 €).", "param": "amount", "doc_url": "https://docs.factuarea.com/guides/errors#business_rule_violation", "request_id": "req_..." } } ``` ### unsupported\_format [#unsupported_format] El format d'exportació/report sol·licitat no està suportat. ### insufficient\_data\_for\_report [#insufficient_data_for_report] No hi ha prou dades per generar el report d'impostos sol·licitat. ### signature\_payload\_too\_large [#signature_payload_too_large] La imatge de signatura de l'albarà supera la mida màxima. ### authentication\_error [#authentication_error] ### missing\_api\_key [#missing_api_key] No hi ha header d'autenticació present (`Authorization: Bearer` o `X-API-Key`). ### invalid\_api\_key [#invalid_api_key] La key no existeix o el secret no coincideix amb el hash emmagatzemat. ### api\_key\_revoked [#api_key_revoked] La key va ser revocada. Crea'n una de nova al dashboard. ### too\_many\_auth\_failures [#too_many_auth_failures] S'han limitat fallades d'autenticació repetides des del teu client. Espera (back off) i verifica les teves credencials. ### payment\_required\_error [#payment_required_error] Tot `402` ve d'una operació que cobra alguna cosa en el moment en què la crides: un seient d'empresa gestionada, un seient d'empleat o un add-on. Cap no es pot reintentar tal com està: resol abans el pagament i repeteix la mateixa petició. **Nota de versió.** Cinc d'aquests codis es van publicar abans que existís aquesta categoria i se servien com a `invalid_request_error`. Porten `payment_required_error` des de [`Factuarea-Version: 2026-09-01`](/guides/versioning) endavant: `payment_method_required`, `seat_charge_failed`, `gestoria_plan_required`, `employee_seat_payment_method_required` i `employee_seat_charge_failed`. Les peticions en una versió anterior conserven el `type` de sempre. `error.code`, `error.subcode` i l'estat `402` són idèntics a totes les versions: ramifica per `code` i no hauràs de pensar en això. ### payment\_method\_required [#payment_method_required] `POST /v1/companies` i els endpoints d'activació cobren un seient immediatament, i la gestoria opera en mode real sense mètode de pagament configurat. La resposta porta `details.payment_setup_url`: obre'l, registra una targeta i repeteix la crida. ```json { "error": { "type": "payment_required_error", "code": "payment_method_required", "message": "La gestoría no tiene un método de pago configurado: configúralo para añadir la empresa.", "details": { "payment_setup_url": "https://billing.stripe.com/p/session/live_YWNjdF8xS2ZHM0RLb0h4RXBGV3lY" }, "request_id": "req_01HKQS5NPAYMENTMETHODREQ01", "doc_url": "https://docs.factuarea.com/guides/errors#payment_method_required" } } ``` ### seat\_charge\_failed [#seat_charge_failed] El càrrec prorratejat del seient de l'empresa gestionada va ser denegat —targeta rebutjada, autenticació requerida, o el proveïdor de pagament inaccessible—. L'empresa **no** es crea si el seient no es cobra. Arregla el mètode de pagament al portal de facturació i reintenta. ### gestoria\_plan\_required [#gestoria_plan_required] La gestoria no té una subscripció de pagament activa, així que no hi ha subscripció sobre la qual cobrar el seient. Contracta un pla (o reprèn el que va cancel·lar) abans d'afegir empreses gestionades. ### employee\_seat\_payment\_method\_required [#employee_seat_payment_method_required] Donar d'alta o reactivar un empleat cobra un seient immediatament, i l'empresa opera en mode real sense mètode de pagament configurat. El remei és el mateix que a `payment_method_required`, i la resposta també porta `details.payment_setup_url`. ### employee\_seat\_charge\_failed [#employee_seat_charge_failed] El càrrec prorratejat del seient d'empleat va ser denegat. L'empleat **no** s'activa si el seient no es cobra. Arregla el mètode de pagament i reintenta; consulta amb el teu banc si la targeta es continua denegant. ### addon\_required [#addon_required] L'operació pertany a un add-on que l'empresa no ha contractat: per exemple, `POST /v1/webhook_endpoints` requereix l'add-on Developer API, el nivell gratuït del qual permet zero endpoints (`subcode: webhooks_addon_required`). Contracta l'add-on i repeteix la crida. A diferència d'[`addon_not_active`](#addon_not_active) (`403`), aquí el que falta és la contractació, no el scope. ### authorization\_error [#authorization_error] ### insufficient\_scope [#insufficient_scope] La key no té el scope que requereix l'endpoint. Consulta el catàleg a [Autenticació › Scopes](/guides/authentication#scopes). ### permission\_error [#permission_error] ### feature\_not\_available\_in\_plan [#feature_not_available_in_plan] El pla actual no inclou el mòdul requerit (p. ex. `recurring_invoices`). ### addon\_not\_active [#addon_not_active] L'empresa no té un pla de Factuarea actiu que inclogui accés a l'API pública — per exemple, el trial de 10 dies va caducar o la subscripció va vèncer fora del seu període de gràcia. Contracta o renova un pla per continuar fent servir l'API. ### not\_found\_error [#not_found_error] ### resource\_not\_found [#resource_not_found] El recurs no existeix o no pertany a la teva empresa. ### tax\_report\_not\_found [#tax_report_not_found] El report d'impostos sol·licitat no existeix. ### conflict\_error [#conflict_error] ### resource\_already\_exists [#resource_already_exists] Intent de crear un duplicat (p. ex. un `tax_id` ja registrat). El `subcode` (p. ex. `tax_id_already_exists`) assenyala la clau duplicada. ### resource\_conflict [#resource_conflict] L'operació entra en conflicte amb l'estat actual del recurs (p. ex. una modificació concurrent). ### max\_api\_keys\_exceeded [#max_api_keys_exceeded] L'empresa ha assolit el seu nombre màxim d'API keys actives. ### idempotency\_error [#idempotency_error] ### idempotency\_key\_reused [#idempotency_key_reused] Mateix `Idempotency-Key`, body de petició diferent. Usa una key nova. Consulta [Idempotència](/guides/idempotency). ### rate\_limit\_error [#rate_limit_error] ### rate\_limit\_exceeded [#rate_limit_exceeded] Vas superar la quota per minut o mensual del teu tier. El header `Retry-After` indica els segons a esperar. Consulta [Límits de peticions](/guides/rate-limits). ### api\_error [#api_error] ### internal\_error [#internal_error] Error inesperat. Ja està capturat per la nostra banda, però comparteix el `request_id` amb suport. ### service\_unavailable\_error [#service_unavailable_error] ### service\_unavailable [#service_unavailable] L'API pública no està disponible temporalment — deshabilitada globalment via kill-switch, en una finestra de manteniment, o una dependència (base de dades, mailer, Stripe) no està sana. Reintenta després d'un back-off curt. ## Errors tipats amb el SDK oficial [#errors-tipats-amb-el-sdk-oficial] Els [SDKs de TypeScript i PHP](/sdks) mapegen aquest embolcall a una jerarquia d'excepcions tipada, així ramifiques segons una classe (i llegeixes `code`, `type`, `param`, `request_id`) en lloc de parsejar JSON. La teva API key mai no s'inclou en cap excepció. ```ts import { FactuareaError, ValidationError, RateLimitError, } from "@factuarea/sdk"; try { await factuarea.invoices.create(body); } catch (error) { if (error instanceof ValidationError) { console.error(error.fields); // { client_id: ["obligatorio"], … } } else if (error instanceof RateLimitError) { console.error(error.retryAfter); // seconds to wait } else if (error instanceof FactuareaError) { console.error(error.code, error.type, error.requestId); } } ``` La jerarquia també exporta `AuthenticationError`, `NotFoundError`, `ConflictError`, `ServerError` i `ConnectionError`. ```php use Factuarea\Sdk\Models\Errors\ErrorThrowable; try { $factuarea->invoices->publicApiV1InvoicesCreate($body); } catch (ErrorThrowable $e) { $error = $e->container->error; echo $error->type->value; // e.g. "invalid_request_error" echo $error->code; // e.g. "parameter_invalid" echo $error->param; // e.g. "client_id" echo $error->requestId; // quote this to support } ``` Consulta [SDKs › Gestió d'errors](/sdks#handling-errors) per veure la jerarquia completa. La política de reintents de sota l'apliquen automàticament ambdós SDKs. ## request\_id i suport [#request_id-i-suport] Tota resposta inclou un `request_id`. Adjunta'l a qualsevol tiquet o petició a `support@factuarea.com`: ``` Subject: 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV ``` Amb el `request_id` correlacionem logs, mètriques i traces per investigar ràpid. ## Estratègia de reintents [#estratègia-de-reintents] * `4xx` excepte `429` → **no reintentis**: l'error és a la petició. Corregeix-lo i reenvia. * `429` → respecta el header `Retry-After`. Implementa back-off exponencial amb jitter. * `5xx` → back-off exponencial (`2^n * 100ms`) amb jitter, màxim 5 intents. Stripe publica un patró canònic que també aplica aquí: [stripe.com/docs/error-handling](https://stripe.com/docs/error-handling). --- # Tots els error codes (/ca/guides/errors/all) Aquesta és la referència canònica de **tots** els `code` d'error que pot retornar l'API pública, agrupats pel bounded context que els emet. Cada `code` és estable entre versions; el `message` és només per mostrar. El total i l'agrupació es generen del catàleg en viu. ## Compte [#compte] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------- | | [`account_not_found`](/ca/errors/account_not_found) | `not_found_error` | 404 | No es va poder resoldre el compte associat a la clau, cosa que sol voler dir que la clau ja no apunta a una empresa viva. | | [`api_key_already_revoked`](/ca/errors/api_key_already_revoked) | `invalid_request_error` | 422 | La clau ja estava revocada, i una clau revocada no admet més operacions: la revocació és terminal. | | [`api_key_not_found`](/ca/errors/api_key_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap clau API de l'empresa autenticada. | ## Autenticació [#autenticació] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ---------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`api_key_expired`](/ca/errors/api_key_expired) | `authentication_error` | 401 | La clau va passar la seva data de caducitat. | | [`api_key_revoked`](/ca/errors/api_key_revoked) | `authentication_error` | 401 | La clau va ser revocada, i una clau revocada no torna a autenticar mai: revocar és justament la manera de tallar una credencial filtrada. | | [`invalid_api_key`](/ca/errors/invalid_api_key) | `authentication_error` | 401 | La clau no correspon a cap clau activa. Pot estar mal copiada, truncada, o pertànyer a un altre entorn: les claus de prova i les de producció no són intercanviables. | | [`ip_not_allowed`](/ca/errors/ip_not_allowed) | `authentication_error` | 401 | La clau restringeix les adreces que accepta, i la petició va arribar des d'una que no és a la llista. | | [`missing_api_key`](/ca/errors/missing_api_key) | `authentication_error` | 401 | La petició no porta credencials: ni capçalera `Authorization` ni `X-API-Key`. | | [`origin_not_allowed`](/ca/errors/origin_not_allowed) | `authentication_error` | 401 | La petició ve d'un origen de navegador que la clau no accepta. | | [`too_many_auth_failures`](/ca/errors/too_many_auth_failures) | `authentication_error` | 429 | Van arribar massa intents fallits d'autenticació des de la mateixa adreça, així que queda bloquejada temporalment per frenar els intents d'endevinar credencials. | ## Autorització [#autorització] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------- | --------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_not_active`](/ca/errors/addon_not_active) | `authorization_error` | 403 | La funcionalitat pertany a un add-on que ara mateix no està actiu per a l'empresa. | | [`feature_not_available_in_plan`](/ca/errors/feature_not_available_in_plan) | `authorization_error` | 403 | La funcionalitat no està inclosa en el pla de l'empresa. | | [`forbidden_action`](/ca/errors/forbidden_action) | `authorization_error` | 403 | L'acció està bloquejada per a aquest recurs encara que l'abast sigui el correcte: el recurs pertany a un catàleg compartit, o el canvi va per un altre endpoint. | | [`insufficient_scope`](/ca/errors/insufficient_scope) | `authorization_error` | 403 | La clau autentica correctament però no porta l'abast que exigeix aquesta operació. Els abasts es concedeixen en emetre la clau i no s'amplien en temps de crida. | | [`max_api_keys_exceeded`](/ca/errors/max_api_keys_exceeded) | `authorization_error` | 422 | L'empresa va arribar al nombre de claus API que permet el seu pla. | | [`max_webhook_endpoints_exceeded`](/ca/errors/max_webhook_endpoints_exceeded) | `authorization_error` | 422 | L'empresa va arribar al nombre d'endpoints de webhook que permet el seu nivell d'add-on. | | [`module_not_available_in_sandbox`](/ca/errors/module_not_available_in_sandbox) | `authorization_error` | 403 | El recurs pertany a un mòdul vetat en mode test. La sandbox mai toca l'AEAT, els bancs ni cobraments reals, així que aquests mòduls queden fora a propòsit. | | [`scope_not_allowed_by_plan`](/ca/errors/scope_not_allowed_by_plan) | `authorization_error` | 422 | Un dels abasts demanats pertany a un mòdul que el pla no inclou, així que la clau naixeria amb un permís que mai podria exercir. | | [`scope_not_allowed_in_sandbox`](/ca/errors/scope_not_allowed_in_sandbox) | `authorization_error` | 422 | Una clau de prova no pot néixer amb abasts de mòduls vetats a la sandbox. | ## Clients [#clients] | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`alternative_id_type_invalid`](/ca/errors/alternative_id_type_invalid) | `invalid_request_error` | 422 | El tipus d'identificador alternatiu queda fora del catàleg `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. | | [`cannot_have_both_tax_id_and_alternative_id`](/ca/errors/cannot_have_both_tax_id_and_alternative_id) | `invalid_request_error` | 422 | El client envia `tax_id` i un identificador alternatiu alhora. La identitat fiscal és una: l'identificador alternatiu existeix precisament per a parts sense NIF espanyol. | | [`census_requires_tax_id`](/ca/errors/census_requires_tax_id) | `invalid_request_error` | 422 | La verificació censal contrasta el parell nom + NIF contra l'AEAT, i en falta un dels dos. | | [`client_has_documents`](/ca/errors/client_has_documents) | `invalid_request_error` | 422 | El client està referenciat per documents emesos. Esborrar-lo deixaria factures, pressupostos o albarans sense la part a qui es van emetre, i els registres fiscals han de seguir sent traçables. | | [`client_import_too_large`](/ca/errors/client_import_too_large) | `invalid_request_error` | 422 | El CSV supera el límit de files que admet la importació síncrona, ja que el fitxer sencer es processa dins de la mateixa petició. | | [`client_not_found`](/ca/errors/client_not_found) | `not_found_error` | 404 | L'identificador no resol a cap client de l'empresa autenticada. | | [`client_requires_tax_identity`](/ca/errors/client_requires_tax_identity) | `invalid_request_error` | 422 | El client no té identitat fiscal: ni `tax_id` ni identificador alternatiu, i no es pot emetre una factura a una part sense identificar. | | [`direct_debit_requires_default_bank_account`](/ca/errors/direct_debit_requires_default_bank_account) | `invalid_request_error` | 422 | Es va triar domiciliació bancària com a mètode de pagament, però el client no té compte bancari per defecte on carregar. | | [`tax_id_already_exists`](/ca/errors/tax_id_already_exists) | `conflict_error` | 409 | Un altre client de l'empresa ja té aquest NIF, i el NIF identifica la part sense ambigüitat dins d'una empresa. | ## Empreses [#empreses] | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`company_inactive`](/ca/errors/company_inactive) | `authorization_error` | 403 | El perfil que indica `X-Active-Profile` és una de les teves empreses gestionades, però està desactivada i no es pot operar fins que torni a estar activa. | | [`gestoria_module_required`](/ca/errors/gestoria_module_required) | `authorization_error` | 403 | La gestoria té un pla vigent, però sense el mòdul de gestoria, així que no pot crear ni operar empreses gestionades. | | [`gestoria_plan_required`](/ca/errors/gestoria_plan_required) | `payment_required_error` | 402 | La gestoria no té una subscripció de pagament activa, així que no hi ha subscripció sobre la qual cobrar el seient. | | [`payment_method_required`](/ca/errors/payment_method_required) | `payment_required_error` | 402 | Donar d'alta una empresa gestionada cobra un seient immediatament, i la gestoria opera en mode real sense mètode de pagament configurat. | | [`seat_charge_failed`](/ca/errors/seat_charge_failed) | `payment_required_error` | 402 | El cobrament immediat del prorrateig del seient va ser rebutjat: la targeta es va denegar, necessita autenticació, o el proveïdor de pagament era inaccessible. L'empresa no es crea si el seient no es cobra. | ## Albarans [#albarans] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------- | | [`delivery_note_not_found`](/ca/errors/delivery_note_not_found) | `not_found_error` | 404 | L'identificador no resol a cap albarà de l'empresa autenticada. | | [`delivery_note_section_not_editable_in_status`](/ca/errors/delivery_note_section_not_editable_in_status) | `invalid_request_error` | 422 | La secció logística —transportista, vehicle, conductor— està congelada perquè l'albarà ja està lliurat, facturat o cancel·lat. | | [`driver_tax_id_requires_name`](/ca/errors/driver_tax_id_requires_name) | `invalid_request_error` | 422 | Es va enviar el NIF del conductor sense el seu nom, i un identificador sense nom no identifica ningú al document de lliurament. | | [`signature_payload_too_large`](/ca/errors/signature_payload_too_large) | `invalid_request_error` | 422 | La imatge de la signatura supera la mida admesa per al camp. | ## Empleats [#empleats] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------------------- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`employee_seat_charge_failed`](/ca/errors/employee_seat_charge_failed) | `payment_required_error` | 402 | El cobrament immediat del prorrateig del seient d'empleat va ser rebutjat: la targeta es va denegar, necessita autenticació, o el proveïdor de pagament era inaccessible. L'empleat no s'activa si el seient no es cobra. | | [`employee_seat_payment_method_required`](/ca/errors/employee_seat_payment_method_required) | `payment_required_error` | 402 | Donar d'alta o reactivar un empleat cobra un seient immediatament, i l'empresa opera en mode real sense mètode de pagament configurat. | ## Events [#events] | Code | Type | HTTP | Descripció | | ----------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | [`event_not_found`](/ca/errors/event_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap esdeveniment de l'empresa autenticada, o l'esdeveniment va ser purgat per la política de retenció de 30 dies. | ## Idempotency [#idempotency] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`idempotency_key_in_use`](/ca/errors/idempotency_key_in_use) | `idempotency_error` | 409 | Hi ha una altra petició amb la mateixa `Idempotency-Key` encara en curs, i encara no se'n coneix el resultat. | | [`idempotency_key_invalid`](/ca/errors/idempotency_key_invalid) | `invalid_request_error` | 400 | La `Idempotency-Key` no encaixa amb el format admès: entre 1 i 255 caràcters ASCII imprimibles. | | [`idempotency_key_reused`](/ca/errors/idempotency_key_reused) | `idempotency_error` | 409 | Aquesta `Idempotency-Key` ja es va fer servir amb un payload diferent. La clau identifica una operació concreta, així que reutilitzar-la per a una altra buidaria de sentit el replay. | ## Factures [#factures] | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`corrective_invoice_inanulable`](/ca/errors/corrective_invoice_inanulable) | `invalid_request_error` | 422 | La factura és al seu torn una rectificativa, i les rectificatives no s'anul·len mai: la cadena de correcció ha de seguir sent auditable de punta a punta. | | [`export_limit_exceeded`](/ca/errors/export_limit_exceeded) | `invalid_request_error` | 422 | La selecció filtrada supera el límit de 5.000 factures de l'exportació, així que el fitxer es rebutja d'entrada en lloc de truncar-se en silenci. | | [`invalid_correction_nature`](/ca/errors/invalid_correction_nature) | `invalid_request_error` | 422 | `correction_nature` només accepta `S` (substitució: la rectificativa porta els imports corregits complets) o `I` (per diferències: només porta el delta). | | [`invalid_correction_reason`](/ca/errors/invalid_correction_reason) | `invalid_request_error` | 422 | El motiu de rectificació queda fora de la llista fiscal tancada (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), que mapeja als codis AEAT R1 a R4. | | [`invalid_invoice_id`](/ca/errors/invalid_invoice_id) | `invalid_request_error` | 400 | La referència de factura rebuda no és un identificador vàlid; sol voler dir que s'ha colat un valor intern on l'API espera l'`id` públic. | | [`invalid_invoice_number`](/ca/errors/invalid_invoice_number) | `invalid_request_error` | 422 | El número de factura no segueix el format canònic `SÈRIE-AAAA-NNN`, més el sufix `-RECn` a les rectificatives. | | [`invalid_invoice_status`](/ca/errors/invalid_invoice_status) | `invalid_request_error` | 422 | El valor enviat com a estat de factura queda fora del catàleg del cicle de vida (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). | | [`invalid_invoice_uuid`](/ca/errors/invalid_invoice_uuid) | `invalid_request_error` | 400 | L'identificador de factura de la ruta o del payload no és un UUID vàlid. | | [`invalid_payment_method`](/ca/errors/invalid_payment_method) | `invalid_request_error` | 422 | El mètode de pagament queda fora de l'allowlist tancada: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. | | [`invoice_already_annulled`](/ca/errors/invoice_already_annulled) | `invalid_request_error` | 422 | La factura ja estava anul·lada. L'anul·lació és terminal i, amb VeriFactu actiu, el seu registre d'anul·lació ja va arribar a l'AEAT. | | [`invoice_already_paid`](/ca/errors/invoice_already_paid) | `invalid_request_error` | 422 | La factura ja està cobrada. `paid` és un estat terminal i comptablement tancat: l'IVA repercutit ja s'ha declarat, o es declararà en el període. | | [`invoice_already_sent`](/ca/errors/invoice_already_sent) | `invalid_request_error` | 422 | La factura ja va ser emesa: té número definitiu de sèrie i, amb VeriFactu actiu, l'alta a l'AEAT. L'emissió no passa dues vegades. | | [`invoice_cannot_assign_number`](/ca/errors/invoice_cannot_assign_number) | `invalid_request_error` | 422 | Es va demanar número definitiu per a una factura que no és esborrany, o que ja en té. La numeració de sèrie és monòtona i els números no es reassignen. | | [`invoice_invalid_status_transition`](/ca/errors/invoice_invalid_status_transition) | `invalid_request_error` | 422 | L'estat destí no és assolible des de l'actual. El cicle de vida és dirigit: `draft` passa a `scheduled` o `sent`, `sent` a `paid`, `overdue` o `annulled`, i `paid`, `cancelled` i `annulled` són terminals. | | [`invoice_not_cancellable_in_current_state`](/ca/errors/invoice_not_cancellable_in_current_state) | `invalid_request_error` | 422 | Cancel·lar retira un esborrany que encara no és fiscalment vinculant, així que només s'aplica mentre la factura està en `draft`. | | [`invoice_not_correctable_in_current_state`](/ca/errors/invoice_not_correctable_in_current_state) | `invalid_request_error` | 422 | Una rectificativa només s'emet contra una factura ja emesa (`sent` o `paid`). Un esborrany, una factura cancel·lada o una anul·lada no tenen res a rectificar. | | [`invoice_not_deletable_in_current_state`](/ca/errors/invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Només s'esborren les factures en `draft` i `cancelled`. Una factura numerada no desapareix mai: la sèrie correlativa ha de seguir sent auditable. | | [`invoice_not_editable_in_current_state`](/ca/errors/invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Només un esborrany admet edició. Un cop emesa, la factura és immutable i el seu contingut queda congelat juntament amb el seu registre fiscal. | | [`invoice_not_eligible_for_action`](/ca/errors/invoice_not_eligible_for_action) | `invalid_request_error` | 422 | L'acció sol·licitada no s'aplica a aquesta factura: el seu tipus o el seu estat actual la deixen fora de l'abast de l'operació. | | [`invoice_not_found`](/ca/errors/invoice_not_found) | `not_found_error` | 404 | L'identificador no resol a cap factura de l'empresa autenticada. Les factures d'una altra empresa responen exactament igual. | | [`invoice_not_modifiable_in_current_state`](/ca/errors/invoice_not_modifiable_in_current_state) | `invalid_request_error` | 422 | El camp que intentes canviar està congelat per a l'estat actual — per exemple el règim fiscal d'una factura anul·lada. | | [`invoice_not_paid`](/ca/errors/invoice_not_paid) | `invalid_request_error` | 422 | Es va demanar un justificant de pagament d'una factura sense cobrament registrat, així que no hi ha res a certificar. | | [`invoice_not_reschedulable_in_current_state`](/ca/errors/invoice_not_reschedulable_in_current_state) | `invalid_request_error` | 422 | Reprogramar mou la data d'emissió d'una factura que espera en `scheduled`, i aquesta factura no està esperant. | | [`invoice_not_schedulable_in_current_state`](/ca/errors/invoice_not_schedulable_in_current_state) | `invalid_request_error` | 422 | Només un esborrany es pot programar: la programació reserva un moment futur d'emissió sense consumir encara número de sèrie. | | [`invoice_not_unschedulable_in_current_state`](/ca/errors/invoice_not_unschedulable_in_current_state) | `invalid_request_error` | 422 | Desprogramar torna la factura de `scheduled` a `draft`, així que només s'aplica mentre segueix esperant a emetre's. | | [`invoice_not_unsendable_in_current_state`](/ca/errors/invoice_not_unsendable_in_current_state) | `invalid_request_error` | 422 | Desfer la marca de lliurament només s'aplica a una factura `sent`: neteja `sent_at` i manté la factura emesa. | | [`invoice_requires_at_least_one_line`](/ca/errors/invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura no porta cap línia d'operació, així que no té base imposable i no es pot emetre. Passa tant quan no envies línies com quan totes les que envies són de suplert: un suplert és una quantitat pagada per compte del client (art. 78.Tres.3 LIVA), no una operació teva. | | [`invoice_year_required_for_ambiguous_number`](/ca/errors/invoice_year_required_for_ambiguous_number) | `invalid_request_error` | 422 | Aquest número de factura existeix en més d'un exercici, així que per si sol no identifica una única factura. | | [`line_total_checksum_mismatch`](/ca/errors/line_total_checksum_mismatch) | `invalid_request_error` | 422 | El `line_total` declarat no coincideix amb el que calcula Factuarea per a aquella línia (quantitat × preu − descompte + IVA − retenció + recàrrec) i la desviació supera el cèntim de tolerància. L'import que es factura i es declara a l'AEAT és sempre el calculat aquí, així que la discrepància vol dir que el teu sistema i la factura emesa no quadrarien. | | [`line_type_invalid`](/ca/errors/line_type_invalid) | `invalid_request_error` | 422 | El tipus de línia queda fora del catàleg tancat `NORMAL` / `SUPLIDO`. Una factura emesa només distingeix dues naturaleses: el que véns tu, que forma base imposable i porta IVA, i el suplert, que són diners avançats en nom i per compte del client i per això queda fora de la base (art. 78.Tres.3 LIVA). | | [`no_invoices_in_period`](/ca/errors/no_invoices_in_period) | `invalid_request_error` | 422 | L'operació trimestral no va trobar factures en el període demanat, així que no hi ha res a empaquetar ni a enviar. | | [`payment_method_invalid`](/ca/errors/payment_method_invalid) | `invalid_request_error` | 422 | La mateixa allowlist tancada que `invalid_payment_method`, reportada quan el valor es rebutja en llegir el camp de mètode de pagament del payload. | | [`reminder_not_applicable`](/ca/errors/reminder_not_applicable) | `invalid_request_error` | 422 | El recordatori de pagament no escau: la factura no està en `sent` ni `overdue`, no hi ha adreça de destinatari, falta l'enllaç públic o està desactivat, o ja va sortir un altre recordatori les últimes 24 hores. | | [`scheduled_for_in_past`](/ca/errors/scheduled_for_in_past) | `invalid_request_error` | 422 | `scheduled_for` no és estrictament futur, així que no hi ha cap espera a reservar. | | [`simplified_invoice_cannot_be_substituted`](/ca/errors/simplified_invoice_cannot_be_substituted) | `invalid_request_error` | 422 | Una de les factures de la llista de substitució no es pot substituir: no és simplificada, està cancel·lada o anul·lada, pertany a una altra empresa, o ja té substitutiva. | | [`simplified_invoice_not_allowed`](/ca/errors/simplified_invoice_not_allowed) | `invalid_request_error` | 422 | L'operació no és elegible per a factura simplificada: supera els 3.000 €, o és un lliurament intracomunitari, una exportació, una operació amb inversió del subjecte passiu, o el client necessita factura completa per deduir l'IVA. | | [`simplified_limit_exceeded`](/ca/errors/simplified_limit_exceeded) | `invalid_request_error` | 422 | Les línies portarien la factura simplificada (F2) per sobre del límit legal absolut de 3.000 € IVA inclòs. | | [`suplido_line_cannot_carry_taxes`](/ca/errors/suplido_line_cannot_carry_taxes) | `invalid_request_error` | 422 | La línia de suplert porta càrrega pròpia: tipus d'IVA, retenció, recàrrec d'equivalència, descompte, clau de règim, causa d'exempció o producte/paquet. Un suplert no és una operació de l'emissor, així que repercutir-hi un impost seria tributar per un lliurament que no has fet, i lligar-lo a un producte mouria un estoc que mai no has venut. | | [`suplido_not_allowed_in_simplified_invoice`](/ca/errors/suplido_not_allowed_in_simplified_invoice) | `invalid_request_error` | 422 | La factura és simplificada (F2) i una simplificada no identifica el destinatari. Sense destinatari identificat no hi ha a qui acreditar el pagament per compte d'altri, així que l'import no admet el tractament de suplert en aquest tipus de factura. | | [`suplido_requires_source_invoice_reference`](/ca/errors/suplido_requires_source_invoice_reference) | `invalid_request_error` | 422 | La línia de suplert no informa `source_invoice_reference`, el número del justificant que el tercer va expedir a nom del client. Sense aquest justificant el pagament no s'acredita com a fet per compte d'altri i Hisenda el tractaria com a base imposable pròpia de l'emissor, amb el seu IVA repercutit. | ## Notificacions [#notificacions] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------ | | [`notification_not_found`](/ca/errors/notification_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap notificació de l'empresa autenticada, o la notificació va quedar fora de la finestra de retenció. | ## Pagaments [#pagaments] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_payment_date`](/ca/errors/invalid_payment_date) | `invalid_request_error` | 422 | La data de pagament queda fora de la finestra admesa: no pot ser anterior a la data d'emissió de la factura ni situar-se al futur. | | [`payout_reconciliation_amount_mismatch`](/ca/errors/payout_reconciliation_amount_mismatch) | `invalid_request_error` | 422 | L'import confirmat no coincideix amb el net de la liquidació, així que la conciliació tancaria amb una diferència que ningú justifica. | | [`receipt_not_available`](/ca/errors/receipt_not_available) | `invalid_request_error` | 422 | No hi ha justificant a emetre perquè el document no té cap cobrament registrat al darrere. | | [`stripe_payout_already_reconciled`](/ca/errors/stripe_payout_already_reconciled) | `invalid_request_error` | 422 | La liquidació ja estava conciliada, i la conciliació és terminal: repetir-la comptabilitzaria dues vegades l'apunt bancari. | | [`stripe_payout_not_found`](/ca/errors/stripe_payout_not_found) | `not_found_error` | 404 | L'identificador no resol a cap liquidació de l'empresa autenticada. | ## Productes [#productes] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | | [`pack_in_use`](/ca/errors/pack_in_use) | `invalid_request_error` | 422 | El pack està referenciat per documents emesos, així que esborrar-lo trencaria la seva composició. | | [`pack_not_found`](/ca/errors/pack_not_found) | `not_found_error` | 404 | L'identificador no resol a cap pack de l'empresa autenticada. | | [`pack_share_link_failed`](/ca/errors/pack_share_link_failed) | `api_error` | 500 | No es va poder generar l'enllaç per compartir el pack. El pack en si no queda afectat. | | [`product_in_use`](/ca/errors/product_in_use) | `invalid_request_error` | 422 | El producte està referenciat per documents emesos o per altres entrades del catàleg, i eliminar-lo deixaria aquestes referències penjant. | | [`product_not_found`](/ca/errors/product_not_found) | `not_found_error` | 404 | L'identificador no resol a cap producte de l'empresa autenticada. | | [`sku_already_exists`](/ca/errors/sku_already_exists) | `conflict_error` | 409 | Un altre producte de l'empresa ja fa servir aquest SKU, i el SKU identifica l'article sense ambigüitat dins del catàleg. | ## Factures proforma [#factures-proforma] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`invalid_expiry_date`](/ca/errors/invalid_expiry_date) | `invalid_request_error` | 422 | La data de venciment és anterior a la d'emissió, o la supera en més de 365 dies. | | [`invalid_proforma_id`](/ca/errors/invalid_proforma_id) | `invalid_request_error` | 400 | La referència de proforma rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. | | [`invalid_proforma_number`](/ca/errors/invalid_proforma_number) | `invalid_request_error` | 422 | El número de proforma no segueix el format canònic de numeració de la seva sèrie. | | [`invalid_proforma_status`](/ca/errors/invalid_proforma_status) | `invalid_request_error` | 422 | El valor enviat com a estat queda fora del catàleg `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. | | [`invalid_proforma_uuid`](/ca/errors/invalid_proforma_uuid) | `invalid_request_error` | 400 | L'identificador de proforma de la ruta o del payload no és un UUID vàlid. | | [`proforma_already_accepted`](/ca/errors/proforma_already_accepted) | `invalid_request_error` | 422 | El client ja va acceptar la proforma, i l'acceptació es registra una sola vegada. | | [`proforma_already_rejected`](/ca/errors/proforma_already_rejected) | `invalid_request_error` | 422 | La proforma ja està marcada com a rebutjada. | | [`proforma_cannot_be_accepted`](/ca/errors/proforma_cannot_be_accepted) | `invalid_request_error` | 422 | L'acceptació no escau des de l'estat actual: una proforma facturada, cancel·lada o expirada ja no l'admet. | | [`proforma_cannot_be_rejected`](/ca/errors/proforma_cannot_be_rejected) | `invalid_request_error` | 422 | El rebuig no escau des de l'estat actual: un cop facturada, cancel·lada o expirada, la proforma està tancada. | | [`proforma_cannot_be_sent`](/ca/errors/proforma_cannot_be_sent) | `invalid_request_error` | 422 | L'enviament per correu no s'aplica a una proforma en estat terminal: no hi ha oferta viva a lliurar. | | [`proforma_invalid_status_transition`](/ca/errors/proforma_invalid_status_transition) | `invalid_request_error` | 422 | L'estat destí no és assolible des de l'actual: un esborrany s'accepta, es cancel·la o expira; una proforma acceptada es factura, es rebutja o expira; facturada, cancel·lada i expirada són terminals. | | [`proforma_not_convertible_in_current_state`](/ca/errors/proforma_not_convertible_in_current_state) | `invalid_request_error` | 422 | Convertir en factura exigeix que el client hagi acceptat la proforma; des de qualsevol altre estat no hi ha acord a facturar. | | [`proforma_not_deletable_in_current_state`](/ca/errors/proforma_not_deletable_in_current_state) | `invalid_request_error` | 422 | Només s'esborra una proforma en esborrany. Un cop acceptada, rebutjada o facturada forma part del rastre comercial. | | [`proforma_not_draft`](/ca/errors/proforma_not_draft) | `invalid_request_error` | 422 | L'operació només té sentit mentre la proforma és un esborrany, i aquesta ja ha avançat. | | [`proforma_not_editable_in_current_state`](/ca/errors/proforma_not_editable_in_current_state) | `invalid_request_error` | 422 | Només una proforma en esborrany admet edició. Un cop acceptada, rebutjada, expirada, facturada o cancel·lada, el seu contingut queda fixat. | | [`proforma_not_found`](/ca/errors/proforma_not_found) | `not_found_error` | 404 | L'identificador no resol a cap proforma de l'empresa autenticada. | | [`proforma_requires_at_least_one_line`](/ca/errors/proforma_requires_at_least_one_line) | `invalid_request_error` | 422 | La proforma no porta línies, així que no hi ha import a posar davant del client. | | [`public_link_expires_at_exceeds_max_days`](/ca/errors/public_link_expires_at_exceeds_max_days) | `invalid_request_error` | 422 | La caducitat demanada per a l'enllaç públic supera la finestra màxima que permet el teu pla per a documents compartits. | ## Factures de compra [#factures-de-compra] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`attachment_invalid_filename`](/ca/errors/attachment_invalid_filename) | `invalid_request_error` | 422 | El nom del fitxer no és utilitzable: és buit, porta components de ruta, o supera els 200 caràcters. | | [`attachment_mime_not_allowed`](/ca/errors/attachment_mime_not_allowed) | `invalid_request_error` | 422 | El tipus de fitxer queda fora del conjunt admès: PDF, PNG, JPEG, XML i HTML. | | [`attachment_missing`](/ca/errors/attachment_missing) | `not_found_error` | 404 | La factura de compra existeix però no té fitxer adjunt, així que no hi ha res a descarregar. | | [`attachment_too_large`](/ca/errors/attachment_too_large) | `invalid_request_error` | 422 | El fitxer supera la mida màxima permesa per a un adjunt de document. | | [`cannot_attach_to_cancelled_purchase_invoice`](/ca/errors/cannot_attach_to_cancelled_purchase_invoice) | `invalid_request_error` | 422 | La factura està cancel·lada, i adjuntar documents a un registre cancel·lat alteraria documentació ja tancada. | | [`invalid_purchase_invoice_id`](/ca/errors/invalid_purchase_invoice_id) | `invalid_request_error` | 400 | La referència de factura de compra rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. | | [`invalid_purchase_invoice_number`](/ca/errors/invalid_purchase_invoice_number) | `invalid_request_error` | 422 | El número de factura és buit o no encaixa amb el format admès. En una factura de compra el número és el que va imprimir el proveïdor, no un que generi Factuarea. | | [`invalid_purchase_invoice_uuid`](/ca/errors/invalid_purchase_invoice_uuid) | `invalid_request_error` | 400 | L'identificador de factura de compra de la ruta o del payload no és un UUID vàlid. | | [`operation_regime_invalid`](/ca/errors/operation_regime_invalid) | `invalid_request_error` | 422 | El règim d'operació queda fora del catàleg `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. | | [`purchase_invoice_already_exists`](/ca/errors/purchase_invoice_already_exists) | `conflict_error` | 409 | Aquest proveïdor ja té registrada una factura de compra amb el mateix número. El parell proveïdor + número identifica el document sense ambigüitat i evita comptabilitzar dues vegades la mateixa despesa. | | [`purchase_invoice_not_deletable_in_current_state`](/ca/errors/purchase_invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Només s'esborren les factures de compra en esborrany o cancel·lades. Una de pendent o pagada forma part del llibre de despeses. | | [`purchase_invoice_not_draft`](/ca/errors/purchase_invoice_not_draft) | `invalid_request_error` | 422 | L'operació només s'aplica mentre la factura de compra és un esborrany, i aquesta ja està registrada. | | [`purchase_invoice_not_editable_in_current_state`](/ca/errors/purchase_invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Només s'edita una factura de compra en esborrany. Un cop registrada com a pendent, pagada o cancel·lada, el seu contingut dona suport a un apunt comptable. | | [`purchase_invoice_not_found`](/ca/errors/purchase_invoice_not_found) | `not_found_error` | 404 | L'identificador no resol a cap factura de compra de l'empresa autenticada. | | [`purchase_invoice_requires_at_least_one_line`](/ca/errors/purchase_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura de compra no porta línies, així que no hi ha despesa ni IVA suportat a registrar. | ## Pressupostos [#pressupostos] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [`quote_already_accepted`](/ca/errors/quote_already_accepted) | `invalid_request_error` | 422 | El pressupost ja estava aprovat, i l'aprovació es registra una sola vegada. | | [`quote_already_rejected`](/ca/errors/quote_already_rejected) | `invalid_request_error` | 422 | El pressupost ja està marcat com a rebutjat. | | [`quote_expired`](/ca/errors/quote_expired) | `invalid_request_error` | 422 | El pressupost va passar la seva data de validesa, així que les condicions ofertes ja no vinculen i no es pot aprovar ni convertir tal com està. | | [`quote_not_found`](/ca/errors/quote_not_found) | `not_found_error` | 404 | L'identificador no resol a cap pressupost de l'empresa autenticada. | ## Límit de peticions [#límit-de-peticions] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ------------------ | ---- | ------------------------------------------------------------------------------------ | | [`monthly_quota_exceeded`](/ca/errors/monthly_quota_exceeded) | `rate_limit_error` | 429 | L'empresa va esgotar la quota mensual de crides que inclou el seu pla. | | [`rate_limit_exceeded`](/ca/errors/rate_limit_exceeded) | `rate_limit_error` | 429 | La clau va enviar més peticions de les que permet el seu ritme a la finestra actual. | ## Factures recurrents [#factures-recurrents] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_frequency_interval`](/ca/errors/invalid_frequency_interval) | `invalid_request_error` | 422 | L'interval és menor que 1, així que la recurrència mai avançaria a una execució següent. | | [`invalid_frequency_type`](/ca/errors/invalid_frequency_type) | `invalid_request_error` | 422 | La freqüència queda fora del catàleg `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. | | [`invalid_holiday_handling`](/ca/errors/invalid_holiday_handling) | `invalid_request_error` | 422 | La política de festius queda fora del catàleg `skip`, `before`, `after`, `same`. | | [`invalid_recurring_invoice_id`](/ca/errors/invalid_recurring_invoice_id) | `invalid_request_error` | 400 | La referència de recurrència rebuda no és un identificador vàlid, normalment perquè un valor intern va substituir l'`id` públic. | | [`invalid_recurring_invoice_uuid`](/ca/errors/invalid_recurring_invoice_uuid) | `invalid_request_error` | 400 | L'identificador de recurrència de la ruta o del payload no és un UUID vàlid. | | [`recurring_already_active`](/ca/errors/recurring_already_active) | `invalid_request_error` | 422 | La recurrència ja està en marxa, així que no hi ha res a activar. Codi antic conservat per compatibilitat: els endpoints actuals reporten això com a `recurring_invoice_already_active`. | | [`recurring_invoice_already_active`](/ca/errors/recurring_invoice_already_active) | `invalid_request_error` | 422 | La recurrència ja està en marxa. | | [`recurring_invoice_already_cancelled`](/ca/errors/recurring_invoice_already_cancelled) | `invalid_request_error` | 422 | La recurrència ja estava cancel·lada, i la cancel·lació és terminal. | | [`recurring_invoice_already_paused`](/ca/errors/recurring_invoice_already_paused) | `invalid_request_error` | 422 | La recurrència ja està pausada, així que pausar-la un altre cop no canvia res. | | [`recurring_invoice_cancelled_cannot_resume`](/ca/errors/recurring_invoice_cancelled_cannot_resume) | `invalid_request_error` | 422 | Una recurrència cancel·lada no es reprèn: la cancel·lació la tanca definitivament, a diferència de la pausa. | | [`recurring_invoice_cannot_run`](/ca/errors/recurring_invoice_cannot_run) | `invalid_request_error` | 422 | La recurrència no pot generar una factura ara mateix: no està en marxa, el seu cicle s'ha acabat, o li falten dades que la factura necessita. `error.message` indica el motiu concret. | | [`recurring_invoice_has_generated_invoices`](/ca/errors/recurring_invoice_has_generated_invoices) | `invalid_request_error` | 422 | La recurrència ja va generar factures, i aquestes factures en depenen per a la seva traçabilitat. | | [`recurring_invoice_not_found`](/ca/errors/recurring_invoice_not_found) | `not_found_error` | 404 | L'identificador no resol a cap recurrència de l'empresa autenticada. | | [`recurring_invoice_requires_at_least_one_line`](/ca/errors/recurring_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La recurrència no porta línies, així que cada factura generada sortiria buida. | | [`recurring_not_active`](/ca/errors/recurring_not_active) | `invalid_request_error` | 422 | L'operació necessita una recurrència en marxa i aquesta està pausada, completada o cancel·lada. Codi antic conservat per compatibilitat amb integracions velles. | ## Request [#request] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`business_rule_violation`](/ca/errors/business_rule_violation) | `invalid_request_error` | 422 | Una invariant del domini va rebutjar l'operació. Aquest codi indica la família; `error.subcode` anomena la regla concreta i `error.message` l'explica. | | [`conflicting_pagination_params`](/ca/errors/conflicting_pagination_params) | `invalid_request_error` | 422 | `starting_after` i `ending_before` van viatjar a la mateixa petició. Recorren la col·lecció en sentits oposats, així que només se'n pot aplicar un. | | [`external_id_already_exists`](/ca/errors/external_id_already_exists) | `conflict_error` | 409 | L'`external_id` amb què concilies contra el teu sistema ja està assignat a un altre objecte del mateix tipus en aquesta empresa. | | [`invalid_param_format`](/ca/errors/invalid_param_format) | `invalid_request_error` | 422 | Un form request antic va rebutjar la forma d'un valor. Els endpoints migrats reporten el mateix com a `parameter_invalid_format` o `parameter_invalid_integer`. | | [`invalid_param_value`](/ca/errors/invalid_param_value) | `invalid_request_error` | 422 | Un form request antic va rebutjar el valor d'un camp. Els endpoints migrats reporten el mateix com a `parameter_invalid_enum` o `parameter_invalid_range`. | | [`invalid_status_transition`](/ca/errors/invalid_status_transition) | `invalid_request_error` | 422 | L'estat sol·licitat no és assolible des de l'estat en què es troba ara mateix el document. | | [`length_required`](/ca/errors/length_required) | `invalid_request_error` | 411 | Va arribar una petició amb body en codificació chunked, sense declarar-ne la mida. L'API necessita conèixer la longitud per avançat per rebutjar payloads excessius abans de carregar-los a memòria. | | [`metadata_too_many_keys`](/ca/errors/metadata_too_many_keys) | `invalid_request_error` | 422 | L'objecte `metadata` supera el límit de 50 claus per recurs. | | [`metadata_value_too_long`](/ca/errors/metadata_value_too_long) | `invalid_request_error` | 422 | Un valor de `metadata` supera els 500 caràcters un cop serialitzat a text. | | [`method_not_allowed`](/ca/errors/method_not_allowed) | `invalid_request_error` | 405 | La ruta existeix però no accepta el verb HTTP utilitzat. | | [`missing_required_param`](/ca/errors/missing_required_param) | `invalid_request_error` | 422 | Un form request antic va detectar que faltava un camp obligatori. Els endpoints ja migrats als parsers canònics reporten el mateix com a `parameter_missing`. | | [`parameter_invalid`](/ca/errors/parameter_invalid) | `invalid_request_error` | 422 | Un value object construït a partir del payload va rebutjar el valor rebut. `error.subcode` diu quin: codi d'impost, codi de país, tipus impositiu, etc. | | [`parameter_invalid_boolean`](/ca/errors/parameter_invalid_boolean) | `invalid_request_error` | 400 | Un paràmetre que ha de ser booleà va rebre un valor fora de les representacions acceptades (`true`/`false`, `1`/`0`). | | [`parameter_invalid_cursor`](/ca/errors/parameter_invalid_cursor) | `invalid_request_error` | 400 | El cursor `starting_after` o `ending_before` no és un UUID vàlid, així que no pot apuntar a cap fila de la col·lecció. | | [`parameter_invalid_empty`](/ca/errors/parameter_invalid_empty) | `invalid_request_error` | 400 | Un paràmetre va arribar amb el valor buit: un filtre `in` sense elements, una comparació sense res després de l'operador, o un filtre d'igualtat amb la cadena buida. | | [`parameter_invalid_enum`](/ca/errors/parameter_invalid_enum) | `invalid_request_error` | 400 | El valor queda fora del conjunt tancat que accepta el paràmetre. En els llistats cobreix a més un operador de filtre diferent de `eq`, `gte`, `lte`, `gt`, `lt`, `in` o `contains`. | | [`parameter_invalid_format`](/ca/errors/parameter_invalid_format) | `invalid_request_error` | 400 | El valor té el tipus correcte però no la forma que exigeix el paràmetre: una data, un patró d'identificador o una capçalera com `Factuarea-Version`. | | [`parameter_invalid_integer`](/ca/errors/parameter_invalid_integer) | `invalid_request_error` | 400 | Un paràmetre que ha de ser un nombre enter va rebre alguna cosa que no es pot interpretar com a tal, per exemple `limit=abc`. | | [`parameter_invalid_iso8601`](/ca/errors/parameter_invalid_iso8601) | `invalid_request_error` | 400 | Un filtre de rang (`gte`, `lte`, `gt`, `lt`) va rebre un valor que no és numèric ni una data ISO 8601. | | [`parameter_invalid_range`](/ca/errors/parameter_invalid_range) | `invalid_request_error` | 400 | Un paràmetre numèric va quedar fora dels seus límits. El cas habitual és `limit`, que ha d'estar entre 1 i 100. | | [`parameter_invalid_string`](/ca/errors/parameter_invalid_string) | `invalid_request_error` | 400 | Un paràmetre que ha de ser text va rebre un array, un objecte o un valor que no es pot llegir com a cadena. | | [`parameter_invalid_url`](/ca/errors/parameter_invalid_url) | `invalid_request_error` | 400 | Un camp que ha de contenir una URL absoluta va rebre un valor que no ho és, normalment perquè li falta l'esquema o l'amfitrió. | | [`parameter_invalid_uuid`](/ca/errors/parameter_invalid_uuid) | `invalid_request_error` | 400 | Un camp d'identificador va rebre un valor que no és un UUID vàlid. Tot `id` de recurs a v1 és un UUID. | | [`parameter_invalid_value`](/ca/errors/parameter_invalid_value) | `invalid_request_error` | 422 | El valor és sintàcticament correcte però no admissible per a aquest recurs: fora del catàleg canònic del camp, o incoherent amb la resta del payload. | | [`parameter_missing`](/ca/errors/parameter_missing) | `invalid_request_error` | 400 | L'endpoint exigeix un paràmetre que la petició no portava. `error.param` diu quin. | | [`parameter_unknown`](/ca/errors/parameter_unknown) | `invalid_request_error` | 400 | La petició porta un paràmetre que l'endpoint no accepta: un filtre fora de la seva allowlist, un camp de `sort` no ordenable, o el `page` de paginació per offset — v1 pagina per cursor. | | [`payload_too_large`](/ca/errors/payload_too_large) | `invalid_request_error` | 413 | El body de la petició supera la mida admesa: 1 MB amb caràcter general, 6 MB als endpoints que accepten fitxers. | | [`profile_not_found`](/ca/errors/profile_not_found) | `not_found_error` | 404 | La capçalera `X-Active-Profile` anomena una empresa que no existeix o que no pertany a l'arbre de gestoria de la clau autenticada. Tots dos casos responen igual perquè l'API mai reveli empreses d'altres tenants. | | [`resource_already_exists`](/ca/errors/resource_already_exists) | `conflict_error` | 409 | Crear l'objecte duplicaria un que ja existeix sota una clau única — NIF, SKU, external id. `error.details.existing_resource_id` apunta a l'objecte que ja ocupa aquest valor. | | [`resource_conflict`](/ca/errors/resource_conflict) | `conflict_error` | 409 | L'operació va xocar amb l'estat actual del recurs i no s'aplica cap codi de conflicte més específic. | | [`resource_immutable`](/ca/errors/resource_immutable) | `invalid_request_error` | 422 | L'objecte està tancat a canvis per a aquesta operació: el seu estat o el seu registre comptable impedeixen modificar-lo. | | [`resource_locked`](/ca/errors/resource_locked) | `conflict_error` | 409 | Una altra operació reté el recurs fins que acaba: les escriptures concurrents sobre el mateix objecte se serialitzen en lloc d'entrellaçar-se. | | [`resource_not_deletable`](/ca/errors/resource_not_deletable) | `invalid_request_error` | 422 | L'objecte existeix, però el seu estat o els seus dependents bloquegen l'esborrat. En els esborrats massius aquest és el codi per fila de cada entrada que no es va poder eliminar. | | [`resource_not_found`](/ca/errors/resource_not_found) | `not_found_error` | 404 | L'identificador no resol a res visible per a l'empresa autenticada. Els objectes d'una altra empresa responen exactament igual, a propòsit. | | [`route_not_found`](/ca/errors/route_not_found) | `not_found_error` | 404 | La ruta no correspon a cap endpoint de v1. Sol ser una errada, un prefix `/v1` absent o una ruta d'una altra àrea de l'API. | | [`unknown_filter`](/ca/errors/unknown_filter) | `invalid_request_error` | 422 | Un llistat va rebre un filtre que no coneix. Els parsers canònics de v1 reporten això com a `parameter_unknown`; aquest codi sobreviu per als endpoints encara sense migrar. | | [`unsupported_api_version`](/ca/errors/unsupported_api_version) | `invalid_request_error` | 400 | La capçalera `Factuarea-Version` està ben formada però anomena una versió fora del conjunt suportat. | | [`unsupported_media_type`](/ca/errors/unsupported_media_type) | `invalid_request_error` | 415 | Una petició amb body va declarar un `Content-Type` diferent de `application/json`. | ## Series [#series] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`cannot_archive_last_default_series`](/ca/errors/cannot_archive_last_default_series) | `invalid_request_error` | 422 | La sèrie és l'única activa del seu tipus de document. Arxivar-la deixaria l'empresa sense numeració disponible i congelaria aquest tipus de document. | | [`document_type_required_for_ambiguous_code`](/ca/errors/document_type_required_for_ambiguous_code) | `invalid_request_error` | 422 | Aquest codi de sèrie existeix per a més d'un tipus de document, així que per si sol no identifica una única sèrie. | | [`invalid_series_code`](/ca/errors/invalid_series_code) | `invalid_request_error` | 422 | El codi de la sèrie és buit, massa llarg, o porta caràcters que no corresponen a un prefix fiscal. | | [`invalid_series_name`](/ca/errors/invalid_series_name) | `invalid_request_error` | 422 | El nom de la sèrie és buit o supera la longitud permesa. | | [`invalid_series_number`](/ca/errors/invalid_series_number) | `invalid_request_error` | 422 | El número inicial no és vàlid: no és un enter positiu, o queda a l'últim número ja emès o per sota, cosa que reemetria números ja consumits. | | [`invalid_series_uuid`](/ca/errors/invalid_series_uuid) | `invalid_request_error` | 400 | L'identificador de sèrie de la ruta o del payload no és un UUID vàlid. | | [`invalid_series_year`](/ca/errors/invalid_series_year) | `invalid_request_error` | 422 | L'exercici no és un any de quatre xifres vàlid per a una sèrie de numeració. | | [`monthly_requires_month_segmented_format`](/ca/errors/monthly_requires_month_segmented_format) | `invalid_request_error` | 422 | El comptador es reinicia cada mes però la màscara de numeració no segrega per mes, així que dos mesos arrencarien al mateix correlatiu i produirien números duplicats dins de l'any. | | [`series_already_archived`](/ca/errors/series_already_archived) | `invalid_request_error` | 422 | La sèrie ja estava arxivada, i l'arxivat no es repeteix: una segona crida indica que el client ha perdut l'estat real. | | [`series_code_immutable_with_documents`](/ca/errors/series_code_immutable_with_documents) | `invalid_request_error` | 422 | Canviar el prefix d'una sèrie que ja va emetre documents reescriuria retroactivament el seu identificador fiscal, mentre els clients i l'AEAT tenen el número original. | | [`series_has_documents`](/ca/errors/series_has_documents) | `invalid_request_error` | 422 | La sèrie ja va numerar documents, així que no es pot eliminar: la seqüència correlativa ha de seguir sent auditable. | | [`series_immutable`](/ca/errors/series_immutable) | `invalid_request_error` | 405 | Les sèries no són editables ni eliminables via API: la continuïtat legal de la numeració exigeix que el seu prefix, el seu any i el seu comptador es quedin com estan. | | [`series_initial_number_creates_gap`](/ca/errors/series_initial_number_creates_gap) | `invalid_request_error` | 422 | El número inicial salta més enllà del següent correlatiu natural havent-hi documents de l'any en curs, i aquest buit a la seqüència no és admissible per a l'AEAT. | | [`series_locked_by_verifactu`](/ca/errors/series_locked_by_verifactu) | `invalid_request_error` | 422 | Com a mínim una factura de la sèrie té un registre de facturació acceptat per l'AEAT, cosa que congela el prefix, l'any i la base de numeració de la sèrie. | | [`series_not_found`](/ca/errors/series_not_found) | `not_found_error` | 404 | L'identificador no resol a cap sèrie de numeració de l'empresa autenticada. | | [`series_type_invalid`](/ca/errors/series_type_invalid) | `invalid_request_error` | 422 | El tipus de document de la sèrie queda fora del catàleg `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`series_year_locked`](/ca/errors/series_year_locked) | `invalid_request_error` | 422 | La sèrie ja va emetre documents en el seu any vigent. Moure l'any deixaria aquests documents apuntant a un exercici buit mentre la seva base imposable és en un altre. | ## Servidor [#servidor] | Code | Type | HTTP | Descripció | | ----------------------------------------------------------------- | --------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`dependency_unavailable`](/ca/errors/dependency_unavailable) | `service_unavailable_error` | 503 | Un servei extern del qual depèn l'operació no va respondre a temps. | | [`face_transmission_failed`](/ca/errors/face_transmission_failed) | `api_error` | 502 | La plataforma FACe —el punt d'entrada de les administracions públiques— era inaccessible o va respondre amb una fallada. El problema és aigües amunt, no a la teva petició. | | [`facturae_signing_failed`](/ca/errors/facturae_signing_failed) | `api_error` | 500 | No es va poder produir la signatura XAdES del fitxer Facturae, normalment perquè el certificat de signatura no és utilitzable en aquell moment. | | [`internal_error`](/ca/errors/internal_error) | `api_error` | 500 | Alguna cosa s'ha trencat al nostre costat en processar la petició. La condició no la provoca el teu payload. | | [`maintenance`](/ca/errors/maintenance) | `service_unavailable_error` | 503 | La plataforma és en finestra de manteniment i les escriptures es retenen a propòsit. | | [`pdf_generation_failed`](/ca/errors/pdf_generation_failed) | `service_unavailable_error` | 503 | El servei de renderitzat no va poder produir el PDF. El document i les seves dades són intactes: el que ha fallat és el fitxer. | | [`register_sealing_failed`](/ca/errors/register_sealing_failed) | `api_error` | 500 | El segellat criptogràfic del registre no es va completar, així que el tancament va quedar sense signar en lloc de segellat amb una signatura trencada. | | [`send_failed`](/ca/errors/send_failed) | `api_error` | 500 | El document no es va lliurar per correu: el proveïdor de correu va rebutjar el missatge o era inaccessible. | | [`service_unavailable`](/ca/errors/service_unavailable) | `service_unavailable_error` | 503 | El servei, o una dependència que necessita, no pot respondre temporalment. | ## Proveïdors [#proveïdors] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [`supplier_has_documents`](/ca/errors/supplier_has_documents) | `invalid_request_error` | 422 | El proveïdor està referenciat per factures de compra registrades, i esborrar-lo deixaria aquestes despeses sense la part que les va emetre. | | [`supplier_not_found`](/ca/errors/supplier_not_found) | `not_found_error` | 404 | L'identificador no resol a cap proveïdor de l'empresa autenticada. | ## Informes fiscals [#informes-fiscals] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [`insufficient_data_for_report`](/ca/errors/insufficient_data_for_report) | `invalid_request_error` | 422 | El període no té dades a declarar, o a una factura del període li falta un camp obligatori per a aquest model, típicament el NIF del client. | | [`invalid_period`](/ca/errors/invalid_period) | `invalid_request_error` | 422 | El període no identifica una declaració: l'any queda fora del rang admès, o falta el trimestre o és fora del rang 1 a 4 en un model trimestral. | | [`report_format_invalid`](/ca/errors/report_format_invalid) | `invalid_request_error` | 422 | El format queda fora del catàleg `txt_aeat`, `pdf`, `excel`. | | [`tax_report_not_found`](/ca/errors/tax_report_not_found) | `not_found_error` | 404 | L'identificador no resol a cap declaració de l'empresa autenticada. | | [`tax_report_type_invalid`](/ca/errors/tax_report_type_invalid) | `invalid_request_error` | 422 | El tipus de declaració queda fora del catàleg `modelo_303`, `modelo_347`, `modelo_130`. | | [`unsupported_format`](/ca/errors/unsupported_format) | `invalid_request_error` | 422 | El format demanat no està disponible per a aquest model: no tota declaració produeix totes les sortides. | ## Impostos [#impostos] | Code | Type | HTTP | Descripció | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`custom_tax_creation_disabled`](/ca/errors/custom_tax_creation_disabled) | `authorization_error` | 403 | La creació d'impostos personalitzats està deshabilitada per a aquesta empresa. | | [`duplicate_tax_default_for_document_type`](/ca/errors/duplicate_tax_default_for_document_type) | `invalid_request_error` | 422 | Ja hi ha un altre impost del mateix tipus marcat com a default per a aquest tipus de document, i el parell (tipus d'impost, tipus de document) admet un únic default. | | [`indirect_tax_regime_invalid`](/ca/errors/indirect_tax_regime_invalid) | `invalid_request_error` | 422 | El règim indirecte queda fora del catàleg `iva`, `igic`, `ipsi`. | | [`invalid_aeat_code`](/ca/errors/invalid_aeat_code) | `invalid_request_error` | 422 | El codi d'operació AEAT queda fora del catàleg tancat `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` que fan servir VeriFactu i el SII. | | [`invalid_country_aeat_zone`](/ca/errors/invalid_country_aeat_zone) | `invalid_request_error` | 422 | La zona territorial AEAT queda fora del catàleg `peninsula`, `canarias`, `ceuta`, `melilla`. | | [`invalid_country_code`](/ca/errors/invalid_country_code) | `invalid_request_error` | 422 | El codi de país no té exactament dos caràcters, així que no és un codi ISO 3166-1 alfa-2 vàlid. | | [`invalid_customer_visible_label`](/ca/errors/invalid_customer_visible_label) | `invalid_request_error` | 422 | L'etiqueta que es mostra al client al document supera la longitud permesa. | | [`invalid_description`](/ca/errors/invalid_description) | `invalid_request_error` | 422 | La descripció supera la longitud màxima permesa per al camp. | | [`invalid_document_type`](/ca/errors/invalid_document_type) | `invalid_request_error` | 422 | El tipus de document queda fora del catàleg: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`invalid_rate_for_tax_regime`](/ca/errors/invalid_rate_for_tax_regime) | `invalid_request_error` | 422 | El tipus no pertany a la graella legal del seu règim: l'IGIC admet 0, 3, 5, 7, 9,5, 15 i 20 %; l'IPSI admet 0, 0,5, 1, 2, 4, 8 i 10 %. | | [`invalid_tax_code`](/ca/errors/invalid_tax_code) | `invalid_request_error` | 422 | El codi de l'impost és buit o supera els 50 caràcters. | | [`invalid_tax_name`](/ca/errors/invalid_tax_name) | `invalid_request_error` | 422 | El nom de l'impost és buit o supera els 255 caràcters. | | [`invalid_tax_rate`](/ca/errors/invalid_tax_rate) | `invalid_request_error` | 422 | El tipus impositiu queda fora del rang permès per a la seva classe: IVA 0-27 %, retenció 0-47 %, recàrrec d'equivalència 0-10 %, altres 0-100 %. | | [`invalid_tax_type_filter`](/ca/errors/invalid_tax_type_filter) | `invalid_request_error` | 422 | El filtre `type` del llistat per tipus porta un valor fora de l'enum `vat`, `retention`, `surcharge`, `other`. | | [`invalid_validity_window`](/ca/errors/invalid_validity_window) | `invalid_request_error` | 422 | La finestra de vigència està invertida: `valid_until` és anterior a `valid_from`. | | [`system_tax_default_modification_forbidden`](/ca/errors/system_tax_default_modification_forbidden) | `authorization_error` | 403 | Els defaults dels impostos del catàleg compartit no es fixen sobre l'impost: el catàleg és global i la preferència és de la teva empresa. | | [`system_tax_immutable`](/ca/errors/system_tax_immutable) | `invalid_request_error` | 422 | L'impost pertany al catàleg canònic AEAT que porta el producte. El seu tipus, el seu codi i el seu nom són fixos perquè totes les empreses comparteixin la mateixa referència fiscal. | | [`system_tax_immutable_field`](/ca/errors/system_tax_immutable_field) | `invalid_request_error` | 422 | L'actualització toca un camp congelat en un impost del sistema; `error.param` diu quin. | | [`system_tax_undeletable`](/ca/errors/system_tax_undeletable) | `invalid_request_error` | 422 | Els impostos del sistema formen part del catàleg fiscal compartit i no s'eliminen: esborrar-los trencaria els documents que els referencien. | | [`tax_applies_to_invalid`](/ca/errors/tax_applies_to_invalid) | `invalid_request_error` | 422 | L'àmbit de l'impost queda fora del catàleg `sale`, `purchase`, `both`. | | [`tax_code_already_exists`](/ca/errors/tax_code_already_exists) | `conflict_error` | 409 | Un altre impost del catàleg ja fa servir aquest codi, i el codi identifica l'impost sense ambigüitat. | | [`tax_id_required`](/ca/errors/tax_id_required) | `invalid_request_error` | 422 | L'operació necessita el número d'identificació fiscal (NIF, CIF o NIE) de la part implicada i el registre no en té. | | [`tax_in_use`](/ca/errors/tax_in_use) | `invalid_request_error` | 422 | L'impost està referenciat per documents, productes o proveïdors. Eliminar-lo deixaria documents històrics sense la seva referència fiscal. | | [`tax_inactive_cannot_be_default`](/ca/errors/tax_inactive_cannot_be_default) | `invalid_request_error` | 422 | Un impost desactivat no pot quedar com a default, ni global ni per tipus de document: seria un default ocult que cap formulari pot triar. | | [`tax_not_found`](/ca/errors/tax_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap impost del catàleg accessible per a aquesta empresa. | | [`tax_type_invalid`](/ca/errors/tax_type_invalid) | `invalid_request_error` | 422 | El tipus d'impost queda fora del catàleg `vat`, `retention`, `surcharge`, `other`. | ## VeriFactu [#verifactu] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alta_record_not_found`](/ca/errors/alta_record_not_found) | `not_found_error` | 404 | La factura no té registre d'alta, així que l'operació que en depèn no té sobre què treballar. | | [`anulacion_record_already_exists`](/ca/errors/anulacion_record_already_exists) | `conflict_error` | 409 | La factura ja té un registre d'anul·lació a la cadena, i l'anul·lació es declara una sola vegada. | | [`certificate_expired`](/ca/errors/certificate_expired) | `invalid_request_error` | 422 | El certificat està fora de la seva finestra de validesa: ha caducat, o encara no és vàlid. | | [`certificate_nif_mismatch`](/ca/errors/certificate_nif_mismatch) | `invalid_request_error` | 422 | El NIF del titular del certificat no coincideix amb el de l'empresa. Els registres AEAT es signen en nom de l'empresa, així que tots dos han de ser el mateix. | | [`certificate_not_found`](/ca/errors/certificate_not_found) | `not_found_error` | 404 | L'empresa no té cap certificat FNMT que correspongui a l'identificador, o no en té cap de pujat. | | [`certificate_too_large`](/ca/errors/certificate_too_large) | `invalid_request_error` | 422 | El fitxer supera el límit de 100 KB, quan un certificat FNMT real pesa uns pocs kilobytes. | | [`clock_drift_exceeded`](/ca/errors/clock_drift_exceeded) | `invalid_request_error` | 422 | El rellotge del servidor es va desviar de l'NTP per sobre del marge permès. La marca de temps de generació entra a l'empremta AEAT, així que un rellotge desincronitzat produiria registres que l'AEAT rebutja. | | [`declaracion_already_exists`](/ca/errors/declaracion_already_exists) | `conflict_error` | 409 | L'empresa ja té presentada la declaració responsable del SIF d'aquest període. | | [`declaracion_not_found`](/ca/errors/declaracion_not_found) | `not_found_error` | 404 | L'empresa no té presentada la declaració responsable del SIF del període sol·licitat. | | [`event_already_processed`](/ca/errors/event_already_processed) | `invalid_request_error` | 422 | Aquest esdeveniment del SIF ja consta a la cadena d'esdeveniments, i cada esdeveniment es processa exactament una vegada. | | [`invalid_certificate_format`](/ca/errors/invalid_certificate_format) | `invalid_request_error` | 422 | El fitxer no és un contenidor PKCS#12: els seus primers bytes no corresponen a l'estructura ASN.1 que exigeix el format, digui el que digui l'extensió. | | [`invalid_certificate_password`](/ca/errors/invalid_certificate_password) | `invalid_request_error` | 422 | La contrasenya no obre el fitxer del certificat. | | [`max_retries_exceeded`](/ca/errors/max_retries_exceeded) | `invalid_request_error` | 422 | El registre va esgotar el pressupost de reintents tècnics de reenviament de l'XML emmagatzemat. Reintentar el mateix contingut tornaria a fallar igual. | | [`mode_switch_blocked_until_year_end`](/ca/errors/mode_switch_blocked_until_year_end) | `invalid_request_error` | 422 | El mode VeriFactu es va activar en aquest exercici i ja es va emetre com a mínim un registre de facturació. Fer marxa enrere degradaria la integritat d'una cadena ja declarada a l'AEAT. | | [`record_already_accepted`](/ca/errors/record_already_accepted) | `invalid_request_error` | 422 | L'AEAT ja va acceptar el registre. L'acceptació és terminal i el seu contingut queda congelat com a part de la cadena d'empremtes. | | [`record_immutable`](/ca/errors/record_immutable) | `invalid_request_error` | 422 | El registre pertany a un ledger de només-addició: un cop escrit, el seu contingut fiscal queda tancat a modificacions i a esborrat. | | [`record_not_rejected`](/ca/errors/record_not_rejected) | `invalid_request_error` | 422 | L'esmena només s'aplica a registres que l'AEAT va rebutjar per dades. Aquest registre està en un altre estat — una fallada tècnica, per exemple, la cobreix el reintent automàtic. | | [`record_not_subsanable`](/ca/errors/record_not_subsanable) | `invalid_request_error` | 422 | El registre no es pot esmenar: no és un registre d'alta, o no té factura d'origen des de la qual regenerar-ne el contingut. | | [`requires_annulment`](/ca/errors/requires_annulment) | `invalid_request_error` | 422 | El contingut regenerat canvia un camp que entra a l'empremta —NIF de l'emissor, sèrie i número, data d'expedició, tipus de factura, quota o import total— i la cadena no es pot reescriure. | | [`sii_excluded`](/ca/errors/sii_excluded) | `invalid_request_error` | 422 | L'empresa està registrada al SII, i els obligats al SII queden exclosos del reglament VeriFactu. | | [`verifactu_already_submitted`](/ca/errors/verifactu_already_submitted) | `invalid_request_error` | 422 | La factura ja té el seu registre d'alta. Existeix exactament una alta per factura, així que una segona trencaria la idempotència de la cadena. | | [`verifactu_mode_invalid`](/ca/errors/verifactu_mode_invalid) | `invalid_request_error` | 422 | El mode queda fora del catàleg `verifactu` / `no_verifactu`. | | [`verifactu_not_eligible`](/ca/errors/verifactu_not_eligible) | `invalid_request_error` | 422 | La factura no es pot registrar ara mateix a l'AEAT: l'empresa no està en mode VeriFactu, no té certificat actiu, o el certificat està revocat o emès per a un altre NIF. | | [`verifactu_record_not_found`](/ca/errors/verifactu_record_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap registre de facturació de l'empresa autenticada. | | [`verifactu_transmission_failed`](/ca/errors/verifactu_transmission_failed) | `invalid_request_error` | 422 | L'enviament del registre a l'AEAT no es va completar: l'endpoint era inaccessible o va respondre amb una incidència. | ## Webhooks [#webhooks] | Code | Type | HTTP | Descripció | | ------------------------------------------------------------------------------- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_required`](/ca/errors/addon_required) | `payment_required_error` | 402 | Crear endpoints de webhook pertany a l'add-on Developer API, i l'empresa no el té actiu: el nivell gratuït permet zero endpoints. | | [`api_version_invalid_format`](/ca/errors/api_version_invalid_format) | `invalid_request_error` | 422 | La versió de payload de l'endpoint no és una data `YYYY-MM-DD`. | | [`api_version_unsupported`](/ca/errors/api_version_unsupported) | `invalid_request_error` | 422 | La versió de payload està ben formada però no és entre les que serveix la plataforma. | | [`custom_header_blocklisted`](/ca/errors/custom_header_blocklisted) | `invalid_request_error` | 422 | Una de les capçaleres personalitzades està reservada: la gestiona la capa HTTP (`host`, `content-type`, `content-length`, `user-agent`), l'envia Factuarea com a part del contracte signat (`factuarea-*`), o pertany al proxy (`x-forwarded-*`). | | [`custom_header_value_too_long`](/ca/errors/custom_header_value_too_long) | `invalid_request_error` | 422 | El valor d'una capçalera personalitzada supera els 1024 caràcters. | | [`replay_delivery_not_retryable`](/ca/errors/replay_delivery_not_retryable) | `invalid_request_error` | 422 | Només es reenvien els lliuraments fallits. Un lliurament que va arribar bé, o un encara en curs, no té res a reenviar. | | [`replay_event_expired`](/ca/errors/replay_event_expired) | `invalid_request_error` | 422 | L'esdeveniment que dona suport al lliurament va ser purgat per la política de retenció de 30 dies, així que ja no queda payload a reenviar. | | [`timeout_seconds_out_of_range`](/ca/errors/timeout_seconds_out_of_range) | `invalid_request_error` | 422 | `timeout_seconds` queda fora del rang d'1 a 30 segons. | | [`too_many_custom_headers`](/ca/errors/too_many_custom_headers) | `invalid_request_error` | 422 | L'endpoint declara més de 20 capçaleres personalitzades. | | [`webhook_delivery_not_found`](/ca/errors/webhook_delivery_not_found) | `not_found_error` | 404 | L'identificador no correspon a cap intent de lliurament, o el lliurament queda fora de la finestra de retenció de l'històric. | | [`webhook_endpoint_degraded`](/ca/errors/webhook_endpoint_degraded) | `invalid_request_error` | 422 | L'endpoint està degradat després de fallades repetides de lliurament, així que els pings de prova es rebutgen mentre segueixi en aquest estat. | | [`webhook_endpoint_not_found`](/ca/errors/webhook_endpoint_not_found) | `not_found_error` | 404 | L'identificador no resol a cap endpoint de webhook de l'empresa autenticada. | | [`webhook_secret_recently_rotated`](/ca/errors/webhook_secret_recently_rotated) | `rate_limit_error` | 429 | El secret de signatura es va rotar fa menys de cinc minuts. La finestra de gràcia permet que el teu receptor accepti tots dos secrets durant el canvi; rotar un altre cop dins d'ella invalidaria signatures encara en vol. | --- # Esdeveniments (/ca/guides/events) Cada esdeveniment publicat a Factuarea es **persisteix** com un objecte `event` de només lectura amb un `id` opac (un UUID v7, p. ex. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d`), coherent amb l'`id` de qualsevol altre recurs v1. Això et permet: * Consultar-lo via API: `GET /v1/events/{id}` i `GET /v1/events?type=invoice.paid`. * Entregar-lo als webhook endpoints subscrits (el mateix objecte s'envia al body de l'entrega — vegeu [Webhooks](/guides/webhooks)). * Reenviar una entrega des del dashboard (`Developers > Webhooks > Deliveries`). ## Forma del payload [#forma-del-payload] Cada esdeveniment comparteix aquesta estructura: ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03", "api_version": "2026-05-22", "livemode": true, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } }, "created": "2026-05-15T10:23:18Z" } ``` Camps: * `id` — identificador opac de l'esdeveniment (UUID v7). Fes-lo servir com a idempotency key al teu costat. * `object` — sempre `event`. * `type` — nom de l'esdeveniment com a `.` (p. ex. `invoice.paid`, `quote.approved`). * `aggregate_id` — UUID v7 del recurs que va produir l'esdeveniment (p. ex. la factura per a `invoice.paid`). `null` per a esdeveniments sense agregat reomplert. Diferent d'`id`, que identifica el propi esdeveniment. * `api_version` — versió per data sota la qual es va serialitzar el payload, segellada en l'emissió. Sempre present en els esdeveniments emesos avui; `null` només per a esdeveniments antics emesos abans de segellar les versions. * `livemode` — `true` per a esdeveniments generats en producció (clau live, `fact_live_`); `false` per a esdeveniments generats en mode de prova (empresa sandbox, clau `fact_test_`). Els esdeveniments de mode de prova es registren i es poden consultar via `GET /v1/events`, però **no s'entreguen** als webhook endpoints (vegeu [Mode de prova i sandbox](/guides/test-mode)), de manera que qualsevol esdeveniment que el teu endpoint rebi realment és sempre `livemode: true`. * `data` — una **referència lleugera** al recurs afectat, indexada pel seu tipus — p. ex. `{ "invoice": { "id": "..." } }`. Obtén el recurs des del seu propi endpoint per aconseguir la representació completa i actual. * `created` — timestamp ISO 8601 UTC de quan es va crear l'esdeveniment. ## Idempotència [#idempotència] Cada esdeveniment té un `id` únic. Els webhooks reentreguen el mateix `id` al mateix endpoint a cada reintent. Al teu handler: ```python event_id = event['id'] if seen_in_db(event_id): return '', 200 process(event) mark_seen_in_db(event_id) ``` ## Catàleg d'esdeveniments [#catàleg-desdeveniments] El catàleg complet i autoritatiu de tipus d'esdeveniment subscribibles el retorna `GET /v1/event-catalog`. Cada entrada porta un `name`, una `category`, una `description` llegible i un `status` (`available` o `coming_soon`): ```bash curl https://api.factuarea.com/v1/event-catalog \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ```json { "data": [ { "name": "invoice.paid", "category": "invoice", "description": "Factura pagada", "status": "available" } ], "has_more": false, "next_cursor": null } ``` Tipus d'esdeveniment representatius per categoria (consulta el catàleg per a la llista completa i actualitzada): ### Factures [#factures] `invoice.created`, `invoice.auto_created`, `invoice.corrective_auto_created`, `invoice.subscription_auto_created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.cancelled`, `invoice.annulled`, `invoice.overdue`, `invoice.deleted`, `invoice.number_assigned`, `invoice.rectified`, `invoice.email_sent`, `invoice.email_failed`, `invoice.payment_reminder_sent`, `invoice.simplified_created`, `invoice.simplified_substituted`, `invoice.substituted_by_complete`, `invoice.verifactu_submitted`, `invoice.verifactu_failed`, `invoice.metadata_changed`. `invoice.auto_created` / `invoice.corrective_auto_created` / `invoice.subscription_auto_created` els emeten els fluxos d' [auto-facturació de passarel·les de pagament](/payments/stripe-autoinvoicing) quan un cobrament, una devolució o un cicle de subscripció generen una factura de manera automàtica. ### Pressupostos [#pressupostos] `quote.created`, `quote.updated`, `quote.deleted`, `quote.approved`, `quote.rejected`, `quote.converted`, `quote.expired`, `quote.marked_as_pending`, `quote.cancelled`, `quote.number_assigned`, `quote.metadata_changed`, `quote.email_sent`, `quote.email_failed`. ### Factures proforma [#factures-proforma] `proforma.created`, `proforma.updated`, `proforma.deleted`, `proforma.accepted`, `proforma.rejected`, `proforma.cancelled`, `proforma.expired`, `proforma.converted_to_invoice`, `proforma.number_assigned`, `proforma.metadata_changed`, `proforma.email_sent`, `proforma.email_failed`. ### Albarans [#albarans] `delivery_note.created`, `delivery_note.updated`, `delivery_note.status_changed`, `delivery_note.signed`, `delivery_note.converted`, `delivery_note.email_sent`, `delivery_note.email_failed`. ### Factures de compra [#factures-de-compra] `purchase_invoice.created`, `purchase_invoice.updated`, `purchase_invoice.paid`, `purchase_invoice.payment_registered`, `purchase_invoice.cancelled`, `purchase_invoice.metadata_changed`. ### Factures recurrents [#factures-recurrents] `recurring_invoice.created`, `recurring_invoice.activated`, `recurring_invoice.paused`, `recurring_invoice.updated`, `recurring_invoice.deleted`, `recurring_invoice.completed`, `recurring_invoice.executed`, `recurring_invoice.failed`, `recurring_invoice.cancelled`, `recurring_invoice.metadata_changed`. ### Clients i productes [#clients-i-productes] `client.created`, `client.updated`, `client.deleted`, `client.metadata_changed`, `product.created`, `product.updated`. ### Sèries i impostos [#sèries-i-impostos] `series.created`, `series.updated`, `series.deleted`, `series.archived`, `series.unarchived`, `series.marked_as_default`, `series.demoted_from_default`, `series.number_consumed`, `series.year_reset`, `series.month_reset`, `tax.metadata_changed`, `tax.validity_changed`, `tax.external_reference_changed`, `payment.received`. ### FacturaE (FACe) [#facturae-face] `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested`. ### Pagaments i passarel·les [#pagaments-i-passarelles] `payout.reconciled`. `payout.reconciled` es dispara quan un payout de Stripe es concilia amb el teu extracte bancari (consulta [Payouts i conciliació](/payments/payouts-reconciliation)). ## Exemples de payload [#exemples-de-payload] ### invoice.paid [#invoicepaid] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03", "api_version": "2026-05-22", "livemode": true, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } }, "created": "2026-05-15T11:42:08Z" } ``` ### client.updated [#clientupdated] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a1a", "object": "event", "type": "client.updated", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a2b", "api_version": "2026-05-22", "livemode": true, "data": { "client": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a2b" } }, "created": "2026-05-15T11:50:12Z" } ``` ### quote.converted [#quoteconverted] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a3c", "object": "event", "type": "quote.converted", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d", "api_version": "2026-05-22", "livemode": true, "data": { "quote": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d" } }, "created": "2026-05-15T12:01:55Z" } ``` L'esdeveniment només porta una referència lleugera al recurs afectat. Obtén el recurs des del seu propi endpoint (p. ex. `GET /v1/quotes/{id}`) per llegir la factura convertida a la qual enllaça. ## Subscriure's a esdeveniments [#subscriures-a-esdeveniments] Via API: ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.mycompany.com/factuarea/webhook", "enabled_events": ["invoice.paid", "quote.approved"] }' ``` Per subscriure't a **tots els esdeveniments** (no recomanat en producció excepte per a dashboards interns): ```json { "enabled_events": ["*"] } ``` Per subscriure't a famílies senceres (tots els `invoice.*`): ```json { "enabled_events": ["invoice.*", "quote.*"] } ``` ## Llistar esdeveniments via API [#llistar-esdeveniments-via-api] ```bash GET /v1/events?type=invoice.paid&limit=50 ``` Filtres disponibles: `type`, `type[in]`, `created[gte]`, `created[lte]`, `created[gt]`, `created[lt]`. Paginació per cursor estàndard (`limit`, `starting_after`, `ending_before`) — vegeu [Paginació](/guides/pagination). --- # Exportació i importació (/ca/guides/export-and-import) L'API pública mou dades dins i fora de Factuarea amb dues operacions basades en fitxer: **exportar factures** a un full de càlcul i **importar clients** des d'un CSV. Totes dues reutilitzen els mateixos motors que el tauler, i la importació segueix el contracte [partial-success](/docs/guides/bulk-operations): una fila errònia mai enfonsa el fitxer sencer. ## Exportar factures a un full de càlcul [#exportar-factures-a-un-full-de-càlcul] `POST /v1/invoices/export/excel` (scope `invoices:read`) genera un full XLSX o CSV de les teves factures i retorna el fitxer binari. És una operació de **lectura**: no crea ni modifica res. Dos eixos ortogonals controlen la sortida: | Paràmetre | Valors | Significat | | ------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `format` | `SUMMARY` (per defecte) · `ITEMS` | Disposició del contingut. `SUMMARY` és **una fila per factura**; `ITEMS` és **una fila per línia de factura** (les columnes de capçalera es repeteixen a cada línia). | | `file_format` | `xlsx` (per defecte) · `csv` | Format de fitxer. | Tria les factures a exportar de dues maneres: * **Per id** — passa `invoice_ids` amb els ids UUID v7 de factures concretes. * **Per filtre** — omet `invoice_ids` i acota el conjunt amb `status`, `date_from`, `date_to`, `client_id`, `series_id` i `search`. `date_from` i `date_to` filtren per data d'emissió (totes dues incloses); `client_id` i `series_id` prenen l'UUID v7 públic del client/sèrie. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/export/excel \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "format": "ITEMS", "file_format": "xlsx", "status": "paid", "date_from": "2026-01-01", "date_to": "2026-03-31" }' \ -o factures-t1.xlsx ``` ### El límit de 5000 factures [#el-límit-de-5000-factures] El conjunt seleccionat té un límit de **5000 factures**. Si els teus filtres coincideixen amb més, l'API **no** trunca de manera silenciosa: retorna `422` amb el codi d'error `export_limit_exceeded`: ```json { "error": { "type": "invalid_request_error", "code": "export_limit_exceeded", "message": "La exportación supera el máximo de 5000 facturas." } } ``` Acota el rang de dates, l'estat o el client, o divideix l'exportació en diverses crides, perquè cada petició quedi per sota del límit. `client_id`, `series_id` i les entrades de `invoice_ids` es resolen **dins de la teva empresa**. Un UUID inexistent o d'una altra empresa simplement es descarta de la selecció: mai filtra dades entre empreses ni retorna un `404` global. ## Importar clients des d'un CSV [#importar-clients-des-dun-csv] `POST /v1/clients/import` (scope `clients:write`) llegeix un fitxer delimitat i crea un client per cada fila vàlida. La petició és **`multipart/form-data`** —porta un fitxer, no un cos JSON— amb tres camps: | Camp | Tipus | Significat | | --------- | ------- | --------------------------------------------------------------------------------- | | `file` | fitxer | El fitxer CSV/XLSX/XLS/ODS/TXT, fins a **10 MB**. | | `mapping` | objecte | `{ "capçalera_csv": "camp_destí" }`. Ha de mapejar com a mínim `name` i `tax_id`. | | `dry_run` | booleà | Si és `true`, valida i previsualitza **sense** crear res. Per defecte `false`. | El `mapping` indica a l'importador quina columna del full alimenta cada camp del client. El conjunt de destí **ha d'incloure `name` i `tax_id`**: sense ells no es pot crear un client i la petició es rebutja amb `422` abans de processar cap fila. ### Descarregar la plantilla [#descarregar-la-plantilla] `GET /v1/clients/import/template` retorna un CSV a punt per omplir (UTF-8 amb BOM perquè Excel l'obri bé) la fila de capçalera del qual llista totes les columnes que entén l'importador: `Nombre`, `NIF/CIF`, `Razón social`, `Email`, `Teléfono`, camps d'adreça, IVA/retenció per defecte, IBAN i més. Dues files d'exemple mostren el format esperat. ```bash curl -s https://api.factuarea.com/v1/clients/import/template \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -o plantilla-clients.csv ``` ### Primer dry run, després importar [#primer-dry-run-després-importar] Valida sempre amb `dry_run=true` abans de confirmar. La previsualització retorna un informe per fila i **no escriu res**: ```bash curl -s -X POST https://api.factuarea.com/v1/clients/import \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -F "file=@clients.csv" \ -F 'mapping={"Nombre":"name","NIF/CIF":"tax_id","Email":"email"};type=application/json' \ -F "dry_run=true" ``` ```json { "data": { "object": "client_import_preview", "total_rows": 3, "rows": [ { "row": 2, "status": "valid", "errors": [], "warnings": [] }, { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] }, { "row": 4, "status": "valid", "errors": [], "warnings": [] } ] } } ``` `row` és el **número de línia (en base 1) al fitxer** (la capçalera és la fila 1, així que la primera fila de dades és la 2). `status` és `valid` o `error`; cada ítem de `errors[]` porta el `param` afectat, un `code` estable i un `message` en castellà. Quan la previsualització està neta, reenvia el mateix fitxer i mapatge amb `dry_run=false` (o omet-lo). Només es creen les files vàlides; les rebutjades tornen a `failures[]`, i la resposta segueix la forma partial-success amb un `results[]` per fila: ```json { "data": { "total": 3, "successful": 2, "failed": 1, "failures": [ { "index": 1, "error_code": "invalid_tax_id", "error_message": "El NIF no es válido.", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] } ], "results": [ { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] } ] } } ``` Sempre es compleix `total === successful + failed`. Una fila **duplicada** (un client ja existent, segons la regla de deduplicació) s'**omet** (`skipped`), no falla: compta com a `successful` i no es torna a crear, així que reexecutar el mateix fitxer és segur. Ramifica per `error_code` / `code`, mai pel missatge: el missatge és text en castellà, orientat a persones. Els codis per fila surten del catàleg d'errors v1. ### Límit de mida de fitxer [#límit-de-mida-de-fitxer] La importació v1 és **síncrona** per poder retornar el resultat per fila en la mateixa resposta. Els fitxers tenen un límit de **menys de 200 files**; un fitxer més gran es rebutja amb `422` i el codi `client_import_too_large`. Divideix una llista gran en lots per sota del límit i importa'ls en seqüència. Els rebutjos de `file` (10 MB) i `dry_run`, i els límits de 5000/200, s'apliquen abans d'escriure cap fila. La previsualització dry-run és la manera més barata de caçar files malformades: fes-la servir abans de cada importació real. --- # Facturació FACe (B2G) (/ca/guides/face-invoicing) Facturar a una administració pública espanyola (B2G) és obligatori a través de **FACe**, el punt general d'entrada de factures electròniques (Ley 25/2013). Factuarea genera l'XML **FacturaE 3.2.2** de qualsevol factura emesa, el signa **XAdES-EPES** amb el certificat de la teva empresa i el presenta al web service de FACe — i després segueix rastrejant l'estat de tramitació que informa FACe fins que la factura es paga (o es rebutja). El cicle de vida complet el cobreixen les cinc operacions del grup **FacturaE** de la Referència de l'API: * [Descarregar l'XML FacturaE](/api-reference/facturae/public-api.v1.invoices.facturae) d'una factura — signat o sense signar, amb FACe o sense. * [Enviar una factura a FACe](/api-reference/facturae/public-api.v1.invoices.face_submissions.submit). * [Llistar els enviaments d'una factura](/api-reference/facturae/public-api.v1.invoices.face_submissions.list). * [Recuperar un enviament](/api-reference/facturae/public-api.v1.face_submissions.show) per seguir el seu estat de tramitació. * [Sol·licitar l'anul·lació](/api-reference/facturae/public-api.v1.face_submissions.cancel) d'un enviament. Les lectures usen l'scope `facturae:read`; enviar i anul·lar requereixen `facturae:write`. El mòdul FacturaE està inclòs als plans **Empresario** i **Enterprise**. ## Abans d'enviar [#prerequisites] **Configura els tres codis DIR3 del client.** Tot client administració pública porta tres codis del directori DIR3, cadascun amb el format `^[A-Z][A-Z0-9]{8,9}$` (p. ex. `L01280796`). Configura'ls en crear o actualitzar el client: ```bash curl -X PUT https://api.factuarea.com/v1/clients/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42 \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "dir3_accounting_office": "L01280796", "dir3_managing_body": "L01280796", "dir3_processing_unit": "L01280796" }' ``` | Camp | Rol DIR3 | | ------------------------ | ----------------------- | | `dir3_accounting_office` | Oficina contable (01) | | `dir3_managing_body` | Órgano gestor (02) | | `dir3_processing_unit` | Unidad tramitadora (03) | L'administració t'indica els tres codis (sovint coincideixen); també pots consultar-los al directori públic DIR3. **Puja un certificat de signatura actiu.** FACe només accepta factures **signades**, així que l'enviament requereix el certificat FNMT (PKCS#12) que la teva empresa ja usa per a VeriFactu (`POST /v1/verifactu/certificates`). Sense certificat actiu l'enviament falla amb `signing_certificate_required`. **Emet la factura.** Les factures en esborrany no poden viatjar a FACe — enviar o emetre abans la factura és el que congela el seu contingut legal. Els esborranys responen `invoice_not_emittable_for_facturae`. ## Descarregar l'XML FacturaE [#download] Pots descarregar l'XML en qualsevol moment — per a presentació manual, arxiu o validació — sense involucrar FACe: ```bash curl -OJ https://api.factuarea.com/v1/invoices/0197b1c2-89ab-7def-8123-456789abcdef/facturae \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Amb un certificat actiu el cos va signat XAdES-EPES (política de signatura Facturae v3.1) i el fitxer es diu `.xsig`; sense, l'XML torna sense signar com a `.xml`. El header de resposta `X-Facturae-Signed: true|false` distingeix ambdós casos. Consulta la [referència de l'endpoint](/api-reference/facturae/public-api.v1.invoices.facturae). La descàrrega tolera l'absència de certificat (obtens l'XML sense signar); **l'enviament a FACe no** — FACe exigeix la signatura. ## Enviar a FACe [#submit] L'[operació d'enviament](/api-reference/facturae/public-api.v1.invoices.face_submissions.submit) no porta cos de petició: la factura viatja a la ruta i els codis DIR3 es llegeixen del client en el moment de l'enviament (i es capturen a la submission): ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197b1c2-89ab-7def-8123-456789abcdef/face-submissions \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` Resposta (`201`): ```json { "data": { "id": "0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "object": "face_submission", "invoice_id": "0197b1c2-89ab-7def-8123-456789abcdef", "status": "submitted", "registry_number": "202612345678", "dir3_accounting_office": "L01280796", "dir3_managing_body": "L01280796", "dir3_processing_unit": "L01280796", "error_code": null, "error_message": null, "status_updated_at": "2026-06-12T10:15:00Z", "last_polled_at": null, "created_at": "2026-06-12T10:15:00Z" } } ``` `registry_number` és l'assentament registral de FACe que acredita la presentació — conserva'l per a qualsevol disputa amb l'administració. ## Seguir l'estat de tramitació [#states] FACe informa de com l'administració tramita la factura. Factuarea consulta FACe periòdicament i actualitza cada enviament — recuperar el [detall de l'enviament](/api-reference/facturae/public-api.v1.face_submissions.show) (o l'[historial d'enviaments](/api-reference/facturae/public-api.v1.invoices.face_submissions.list) de la factura) és la manera de seguir el progrés; no hi ha endpoint de refresc a v1: ```bash curl https://api.factuarea.com/v1/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` | `status` | Significat | | ------------------------ | -------------------------------------------------------------------------------------- | | `submitted` | Presentada a FACe; número de registre assignat. | | `registered_rcf` | Registrada al RCF (el registre comptable de factures de l'administració). | | `accounted` | Reconeguda com a obligació comptable per l'administració. | | `paid` | L'administració informa de la factura com a pagada. | | `rejected` | Rebutjada per l'administració — consulta el motiu a FACe i emet una factura corregida. | | `cancellation_requested` | Has sol·licitat l'anul·lació; pendent de confirmació de FACe. | | `cancelled` | Anul·lació confirmada per FACe. | | `error` | Error local de transmissió — `error_code` i `error_message` porten el detall. | Prefereixes push a polling? Subscriu-te als [esdeveniments de webhook](/guides/webhooks) `facturae.face_submitted`, `facturae.face_status_changed` i `facturae.face_cancellation_requested`. ## Sol·licitar l'anul·lació [#cancel] Mentre la factura no s'hagi pagat ni rebutjat pots [sol·licitar-ne l'anul·lació](/api-reference/facturae/public-api.v1.face_submissions.cancel) (anul·lació 4200). El `reason` és obligatori i viatja a FACe: ```bash curl -X POST https://api.factuarea.com/v1/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/cancel \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "reason": "Factura emitida por error al organismo equivocado." }' ``` L'anul·lació només es permet en un estat anul·lable (`submitted`, `registered_rcf`, `accounted`); en cas contrari la crida respon `face_submission_not_cancellable`. L'enviament passa a `cancellation_requested` fins que FACe confirma l'estat final `cancelled`. ## Mode de prova [#sandbox] Amb una clau de prova (`fact_test_`) tot el flux es **simula**: cap crida SOAP arriba a FACe i l'enviament rep un número de registre sintètic amb prefix `FACE-SANDBOX-*`. Les validacions de signatura i DIR3 segueixen aplicant, així que el sandbox exercita els mateixos camins d'error que producció. Consulta el [mode de prova](/guides/test-mode). ## Errors [#errors] | HTTP | `code` / `subcode` | Quan | | ---- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | 404 | `resource_not_found` | La factura o l'enviament no existeix o pertany a una altra empresa. | | 422 | `business_rule_violation` / `invoice_not_emittable_for_facturae` | La factura està en esborrany — emet-la primer. | | 422 | `business_rule_violation` / `client_missing_dir3_codes` | Al client li falta un o més codis DIR3. | | 422 | `business_rule_violation` / `signing_certificate_required` | Sense certificat de signatura actiu — puja'n un via `POST /v1/verifactu/certificates`. | | 422 | `business_rule_violation` / `face_submission_not_cancellable` | L'enviament no està en un estat anul·lable. | | 409 | `resource_already_exists` / `face_submission_already_exists` | Ja existeix un enviament actiu per a la factura. | | 403 | `insufficient_scope` | La clau no té l'scope `facturae:write`. | | 502 | `face_transmission_failed` | El web service de FACe està caigut — no es persisteix res; reintenta més tard. | --- # Receptari fiscal (/ca/guides/fiscal-cookbook) Cada recepta de baix és una seqüència completa de crides, amb el seu equivalent al CLI `factuarea`, i un enllaç a la guia que explica **per què** es fa d'aquesta manera. Les guies porten el raonament fiscal; aquesta pàgina porta l'ordre de les operacions. L'arbre de comandes del CLI es genera a partir del document OpenAPI, així que cada endpoint és assolible o bé com a comanda amb nom o bé a través de la via d'escapament genèrica `factuarea api `. Les receptes fan servir la via d'escapament allà on la forma amb nom seria endevinar; totes dues piquen el mateix endpoint de la v1. Vegeu [Ús del CLI](/cli/usage). Fixa la teva clau un sol cop: ```bash export FACTUAREA_KEY="fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ## Quina relació té aquesta pàgina amb les altres quatre preguntes [#dimensions] Cada guia fiscal respon quatre preguntes sobre el seu escenari. Aquesta pàgina és un receptari, així que les respon per delegació, i ho diu en lloc d'ometre les seccions. ### Quan aplica cada recepta [#when] Es diu al capdamunt de cada recepta com el seu **objectiu**. Les condicions prèvies —quin estat de factura admet quina operació, quins tipus de document són admissibles— pertanyen a la guia enllaçada i no es repeteixen aquí. ### Què envia l'API [#api] És l'única dimensió que la pàgina cobreix del tot: cada recepta mostra la petició completa i el seu equivalent al CLI, amb noms de camp reals del contracte v1. ### Què surt al PDF [#pdf] **No es cobreix aquí.** Cap recepta no canvia el document imprès més enllà del que la seva guia ja descriu — el bloc QR legal, les files de suplerts al bloc de totals, la numeració pròpia de la rectificativa. Vegeu [Suplerts](/guides/disbursements#pdf) i [Factures rectificatives](/guides/corrective-invoices#pdf). ### Què arriba a l'AEAT [#aeat] **No es cobreix aquí.** Les declaracions que produeixen aquestes seqüències es descriuen a [Estats d'enviament VeriFactu](/guides/verifactu-submission-states#aeat) i, per escenari, a cada guia enllaçada. La recepta 1 és l'única el *propòsit* de la qual és observar la declaració, i ho fa llegint el registre de facturació. ## 1 · Emetre una factura i esperar l'acceptació de l'AEAT [#issue-and-wait] **Objectiu:** crear, emetre i confirmar que l'Administració tributària l'ha donada d'alta. **Crear i emetre en una sola crida.** `options.issue_directly` estalvia el pas d'enviament separat, i els dos esdeveniments que dispara no poden produir una alta duplicada — la comanda és idempotent per factura. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Servicio de consultoría", "quantity": 2, "unit_price": 150, "tax_rate": 21, "regime_key": "01" } ], "options": { "issue_directly": true } }' ``` ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","issued_on":"2026-06-01","due_on":"2026-07-01","lines":[{"description":"Servicio de consultoría","quantity":2,"unit_price":150,"tax_rate":21,"regime_key":"01"}],"options":{"issue_directly":true}}' ``` **Consulta el registre de facturació** fins que surti dels estats no finals. Llegeix `status` i, un cop acceptat, `aeat_csv` — aquest és el valor amb què concilies contra l'Administració tributària. ```bash curl https://api.factuarea.com/v1/invoices/{invoice_id}/verifactu \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` ```bash factuarea api get /v1/invoices/{invoice_id}/verifactu --json ``` **O deixa de consultar.** Subscriu-te als esdeveniments de webhook VeriFactu de la factura i reacciona quan arribi el resultat. Vegeu [Webhooks](/guides/webhooks). Fonaments: [Alta automàtica a VeriFactu](/guides/verifactu-auto-submission) per a les comportes que decideixen si arriba a crear-se cap registre, i [Estats d'enviament VeriFactu](/guides/verifactu-submission-states) per a què significa cada estat. ## 2 · Corregir un error d'import [#correct-amount] **Objectiu:** una factura emesa va cobrar de més. Reduir-la sense anul·lar-la. Una correcció a la baixa és una rectificativa **per diferències**, amb imports negatius. `correction_type: "partial"` produeix aquesta naturalesa; una substitució no podria portar una base negativa. ```bash curl -X POST https://api.factuarea.com/v1/invoices/{invoice_id}/corrective \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "correction_reason": "error_importe", "correction_type": "partial", "lines": [ { "description": "Ajuste por error de importe", "quantity": -1, "unit_price": 200, "tax_rate": 21 } ] }' ``` ```bash factuarea api post /v1/invoices/{invoice_id}/corrective -d '{"correction_reason":"error_importe","correction_type":"partial","lines":[{"description":"Ajuste por error de importe","quantity":-1,"unit_price":200,"tax_rate":21}]}' ``` Resposta: `201` amb la factura rectificativa nova i una capçalera `Location`. Llista totes les rectificatives emeses contra l'original amb `GET /v1/invoices/{id}/correctives`. Fonaments: [Factures rectificatives](/guides/corrective-invoices). Si la factura encara està sense cobrar i el que està malament és el document sencer i no un import, mira abans [Anul·lar o rectificar](/guides/annul-vs-correct) — l'anul·lació pot ser l'operació correcta. ## 3 · Substituir factures simplificades per una de completa [#substitute] **Objectiu:** un client que ha anat acumulant diversos tiquets necessita ara una factura deduïble. Una sola crida. Passes el destinatari i les factures simplificades que vols agregar, i reps una factura substitutiva completa, ja emesa: ```bash curl -X POST https://api.factuarea.com/v1/invoices/substitute-simplified \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "simplified_invoice_ids": [ "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f601", "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f602" ], "notes": "Consumos de junio" }' ``` ```bash factuarea api post /v1/invoices/substitute-simplified -d '{"client_id":"…","simplified_invoice_ids":["…","…"],"notes":"Consumos de junio"}' ``` Els originals no s'anul·len: conserven el seu estat fiscal i deixen constància que han estat substituïts. Fonaments: [Factures simplificades o completes](/guides/simplified-vs-full-invoices). ## 4 · Repercutir un suplert [#disbursement] **Objectiu:** facturar els teus honoraris més una taxa que vas pagar per compte del client, sense que la taxa entri a la teva base imposable. La línia de suplert **no porta càrrega fiscal pròpia** i **ha de** portar la referència d'origen. Al seu costat es requereix almenys una línia ordinària. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Honorarios de constitución de sociedad", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }, { "description": "Tasa del Registro Mercantil", "quantity": 1, "unit_price": 150, "line_type": "SUPLIDO", "source_invoice_reference": "RM-2026-0451" } ] }' ``` ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","issued_on":"2026-06-01","due_on":"2026-07-01","lines":[{"description":"Honorarios","quantity":1,"unit_price":1000,"tax_rate":21},{"description":"Tasa del Registro Mercantil","quantity":1,"unit_price":150,"line_type":"SUPLIDO","source_invoice_reference":"RM-2026-0451"}]}' ``` Comprova la resposta: `total` és 1210, `total_disbursements` és 150 i `total_to_pay` és 1360. Cobra i concilia contra `total_to_pay`, no contra `total`. Fonaments: [Suplerts](/guides/disbursements). ## 5 · Facturar a un client de fora de la UE [#export] **Objectiu:** una exportació, exempta per l'art. 21 LIVA. **Crea el client amb una identificació alternativa.** El tipus ha de ser legal per al país — un número d'IVA intracomunitari no ho és. ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Inc", "alternative_id": { "type": "passport", "value": "X1234567", "country_code": "US" } }' ``` **Emet amb l'exempció declarada per línia.** El règim de capçalera és de només lectura per l'API pública, així que l'exempció es declara a la línia: ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "{client_id}", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "notes": "Operación exenta por exportación (art. 21 LIVA)", "lines": [ { "description": "Suministro de equipos", "quantity": 1, "unit_price": 4000, "tax_rate": 0, "exemption_reason": "E2", "regime_key": "02" } ] }' ``` Fonaments: [Clients internacionals](/guides/international-customers) — i llegeix la seva nota sobre la inversió del subjecte passiu abans de donar per fet que la mateixa forma serveix per als serveis. ## 6 · Reparar un registre que l'AEAT ha rebutjat [#repair] **Objectiu:** l'Administració tributària ha rebutjat la declaració per un error de dades. Arreglar-ho sense anul·lar la factura. **Confirma que és un rebuig, no una fallada tècnica.** Un estat `rejected` vol dir que l'AEAT ha llegit la declaració; `error` vol dir que no hi va arribar mai i que es reintenta automàticament. ```bash curl "https://api.factuarea.com/v1/verifactu/records?status=rejected" \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` **Corregeix les dades al seu origen.** La declaració es regenera des de la factura i les dades mestres *actuals* — corregeix el NIF o la raó social del client i els valors nous es recullen. ```bash curl -X PUT https://api.factuarea.com/v1/clients/{client_id} \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{"tax_id": "B12345678"}' ``` **Reenvia.** Sense cos de petició: el contingut es regenera al servidor. ```bash curl -X POST https://api.factuarea.com/v1/verifactu/records/{record_id}/subsanar \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` ```bash factuarea api post /v1/verifactu/records/{record_id}/subsanar --json ``` **Vigila el resultat.** El registre es transmet de nou i acaba acceptat — o rebutjat un altre cop si les dades continuen malament, i en aquest cas pots repetir. En aquest camí no hi ha límit d'intents. Si la resposta és un `422` que t'indica que cal una anul·lació, la correcció toca un camp de la *huella* — el total, el número, la data, el NIF de l'emissor o el tipus de factura — i el registre no es pot reparar al lloc. Fonaments: [Esmena de registres VeriFactu](/guides/verifactu-subsanacion) per a la taula d'errors completa, i [Estats d'enviament VeriFactu](/guides/verifactu-submission-states#retry-vs-subsanar) per a reintent contra esmena. ## Traçabilitat [#traceability] Aquesta pàgina no declara cap regla fiscal pròpia: seqüencia crides els fonaments de les quals s'estableixen en un altre lloc. Cada recepta **hereta** la traçabilitat de la guia que enllaça: | Recepta | Hereta de | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Emetre i esperar | [Alta automàtica a VeriFactu](/guides/verifactu-auto-submission#traceability) · [Estats d'enviament VeriFactu](/guides/verifactu-submission-states#traceability) | | Corregir un import | [Factures rectificatives](/guides/corrective-invoices#traceability) · [Anul·lar o rectificar](/guides/annul-vs-correct#traceability) | | Substituir simplificades | [Factures simplificades o completes](/guides/simplified-vs-full-invoices#traceability) | | Repercutir un suplert | [Suplerts](/guides/disbursements#traceability) | | Facturar fora de la UE | [Clients internacionals](/guides/international-customers#traceability) · [Classificació fiscal i exempcions per línia](/guides/line-tax-classification-and-exemptions#traceability) | | Reparar un registre rebutjat | [Estats d'enviament VeriFactu](/guides/verifactu-submission-states#traceability) | --- # Exemples fiscals de factura (/ca/guides/fiscal-invoice-examples) La facturació espanyola cobreix molts escenaris fiscals — B2B nacional, béns i serveis intracomunitaris, vendes a distància OSS, IGIC a Canàries, IPSI a Ceuta/Melilla, retenció IRPF, recàrrec d'equivalència, operacions exemptes i no subjectes. La part difícil és encertar la combinació correcta de `tax_rate`, `exemption_reason`, `regime_key`, `retention_rate` i `surcharge_rate` a cada línia. Per fer-ho concret, la Referència de l'API inclou **quatre exemples de request amb nom i llestos per enviar** a [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) — un conjunt curat i representatiu, no un per escenari. Cadascun és un payload vàlid que pots copiar, adaptar i enviar: tria el més proper al teu cas al desplegable d'exemples del request body i recolza't en la taula següent i en la guia enllaçada a cada fila per a la resta. ## Els 21 escenaris [#scenarios] Cada fila de sota és un escenari que pots expressar línia a línia amb els camps anteriors. Els quatre marcats amb **★** són a més exemples de request amb nom que pots triar directament al desplegable; els altres disset estan documentats aquí i a la guia enllaçada, però no tenen exemple amb nom al spec — construeix-los a partir del marcat més proper. | Clau de l'escenari | Escenari | Guia | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | ★ `b2b_nacional` | B2B nacional, IVA 21% per línia (règim general AEAT 01). | [Claus de règim](/guides/regime-keys) | | `b2b_nacional_iva_reducido` | IVA reduït (10%) o superreduït (4%), règim 01. | [Classificació per línia](/guides/line-tax-classification-and-exemptions) | | ★ `b2c` | Consumidor final (sense NIF del destinatari; factura simplificada quan escaigui). | [Simplificades o completes](/guides/simplified-vs-full-invoices) | | ★ `intracomunitario_bienes` | Lliurament intracomunitari de béns, exempta `E5` (art. 25 LIVA). | [Clients internacionals](/guides/international-customers) | | `intracomunitario_servicios` | Serveis B2B UE, inversió del subjecte passiu — qualificació `S2` (subjecta i **no** exempta, quota repercutida `0`) derivada del règim de capçalera `isp`, no una causa d'exempció. | [Clients internacionals](/guides/international-customers) | | `oss` | Vendes a distància OSS (IVA del país de destí), `regime_key: 17`. | [Clients internacionals](/guides/international-customers) · [Claus de règim](/guides/regime-keys) | | `igic_canarias` | IGIC a Canàries, `regime_key: 08`. | [Impostos territorials](/guides/territorial-taxes) | | `ipsi_ceuta_melilla` | IPSI a Ceuta / Melilla, `regime_key: 08`. | [Impostos territorials](/guides/territorial-taxes) | | ★ `con_irpf` | Retenció IRPF per línia (`retention_rate`). | [Classificació per línia](/guides/line-tax-classification-and-exemptions) | | `con_recargo_equivalencia` | Recàrrec d'equivalència amb un parell legal IVA↔recàrrec, `regime_key: 18`. | [Classificació per línia](/guides/line-tax-classification-and-exemptions) · [Claus de règim](/guides/regime-keys) | | `exenta_articulo_20` | Exempta per art. 20 LIVA, `exemption_reason: E1`. | [Classificació per línia](/guides/line-tax-classification-and-exemptions) | | `exenta_exportacion` | Exportació fora de la UE, exempta `E2` (art. 21), `regime_key: 02`. | [Clients internacionals](/guides/international-customers) · [Claus de règim](/guides/regime-keys) | | `no_sujeta` | Operació no subjecta, `exemption_reason: N1` / `N2`. | [Classificació per línia](/guides/line-tax-classification-and-exemptions) | | `inversion_sujeto_pasivo_nacional` | Inversió del subjecte passiu nacional (p. ex. execució d'obra), `tax_rate: 0`. | [Classificació per línia](/guides/line-tax-classification-and-exemptions) | | `regimen_especial_bienes_usados` | Règim del marge de béns usats (REBU), `regime_key: 03`. | [Claus de règim](/guides/regime-keys) | | `regimen_agencias_viajes` | Règim d'agències de viatges (REAV), `regime_key: 05`. | [Claus de règim](/guides/regime-keys) | | `criterio_caja` | Règim del criteri de caixa, `regime_key: 07`. | [Claus de règim](/guides/regime-keys) | | `multilinea_iva_mixto` | Diverses línies a tipus d'IVA diferents (21% / 10% / 4%). | [Classificació per línia](/guides/line-tax-classification-and-exemptions) | | `con_descuento_y_metadata` | `discount_percent` per línia més `metadata` d'integració. | [Receptari fiscal](/guides/fiscal-cookbook) | | `con_idempotency_key` | Reintents segurs amb el header `Idempotency-Key`. | [Receptari fiscal](/guides/fiscal-cookbook) | | `cliente_extranjero_alternative_id` | Destinatari estranger amb identificador alternatiu (matriu tipus↔país AEAT). | [Clients internacionals](/guides/international-customers) | Cadascun dels quatre exemples marcats també es publica com una entrada reutilitzable `components.examples.invoice_*` al spec OpenAPI, perquè els SDK i el tooling els puguin resoldre per `$ref`. Els value objects fiscals (règim, motiu d'exempció, IRPF, recàrrec) vénen del motor fiscal de Factuarea. Els exemples mostren combinacions vàlides; per al contracte camp a camp consulta el schema del request de [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) i [Imports i dates](/guides/amounts-and-dates). ## Factures rectificatives per codi R [#r-codes] Una factura rectificativa porta el codi de rectificació de l'AEAT que indica **per què** es corregeix l'original. [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective) inclou un exemple amb nom per cada codi, cadascun un payload vàlid que produeix aquest `correction_code` exacte: | Exemple | Codi | Aplica a | Guia | | ------------------ | ---- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `r1_error_fundado` | `R1` | Error fonamentat de dret / anul·lació. Factures completes F1/F3. | [Factures rectificatives](/guides/corrective-invoices) | | `r2_concurso` | `R2` | Concurs de creditors del destinatari. F1/F3. | [Factures rectificatives](/guides/corrective-invoices) | | `r3_incobrable` | `R3` | Crèdits incobrables. F1/F3. | [Factures rectificatives](/guides/corrective-invoices) | | `r4_otras` | `R4` | Resta de causes; total o parcial (`correction_type: partial` amb `lines`). F1/F3. | [Factures rectificatives](/guides/corrective-invoices) | | `r5_simplificada` | `R5` | Rectificació d'una factura **simplificada**. Només F2. | [Factures rectificatives](/guides/corrective-invoices) · [Simplificades o completes](/guides/simplified-vs-full-invoices) | Passa `correction_code` explícitament per seleccionar el codi R; `R5` només aplica a factures simplificades (F2). Una rectificativa és al seu torn un document fiscal: un cop emesa es reporta a l'AEAT via VeriFactu igual que qualsevol altra factura. Usa l'exemple que coincideixi amb la causa legal — el codi no és cosmètic. --- # Glossari (/ca/guides/glossary) L'API de Factuarea modela conceptes de facturació i compliment fiscal espanyols. Si integres des de fora d'Espanya — o simplement vols una referència precisa — aquest glossari explica els termes del domini que apareixen en noms de camps, valors d'enum i missatges d'error, i com es correspon cadascun amb l'API. Els missatges d'error de l'API (`error.message`) es retornen **en castellà** perquè reflecteixen la resposta real de l'API. Els camps `type`, `code` i `subcode` són identificadors estables en anglès — fes match sobre aquests, no sobre el text del missatge. Consulta [Errors](/guides/errors). ## Identificadors fiscals [#identificadors-fiscals] | Terme | Definició | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **NIF / CIF / NIE** | El número fiscal tributari espanyol. El *NIF* (Número de Identificación Fiscal) identifica residents i empreses, el *CIF* era el codi heretat per a persones jurídiques, i el *NIE* (Número de Identidad de Extranjero) identifica residents estrangers. A l'API tots resideixen en l'únic camp `tax_id` de `clients`, `suppliers` i el teu compte. Per a contraparts no espanyoles usa `alternative_id` al seu lloc — és mútuament excloent amb `tax_id`. | | **VAT ID (NIF intracomunitario)** | Un número d'IVA intracomunitari de la UE, exposat com el camp `vat_id` a `clients` i `suppliers`. Diferent de `tax_id`: identifica la part per a operacions intracomunitàries exemptes d'IVA, no per a finalitats fiscals domèstiques. | | **AEAT** | Agencia Estatal de Administración Tributaria — l'agència tributària espanyola. És la receptora dels registres VeriFactu, l'autoritat darrere de les declaracions [Modelo](#tax-declarations) i l'emissora del [CSV](#verifactu-records--hash-chain). Tots els camps `aeat_*` i els endpoints `/v1/verifactu/aeat-access/*` s'hi relacionen. | ## Impostos [#impostos] | Terme | Definició | | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **IVA (VAT)** | Impuesto sobre el Valor Añadido — l'impost sobre el valor afegit espanyol. A l'API és un impost de `type: "vat"` al catàleg d'impostos. Aplica'l per línia mitjançant `tax_rate_id`; els totals els calcula l'API (`subtotal + total_vat + total_surcharge − total_retention`). Consulta la secció Taxes a l'API Reference. | | **Retención (IRPF withholding)** | Una retenció deduïda d'una línia i remesa a l'AEAT en nom del destinatari, normalment IRPF (Impuesto sobre la Renta de las Personas Físicas) per a autònoms. Es modela com un impost de `type: "retention"`. **Resta** del total del document, a diferència de l'IVA i el recàrrec. | | **Recargo de equivalencia (equivalence surcharge)** | Un règim especial d'IVA per a minoristes: un recàrrec addicional sumat sobre l'IVA perquè el minorista no presenti declaracions d'IVA per separat. Es modela com un impost de `type: "surcharge"`; una contrapart subjecta a ell porta `is_surcharge_subject: true`. **Suma** al total del document. | ## Documents [#documents] | Terme | Definició | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Serie (numbering series)** | La seqüència de numeració correlativa i sense buits a la qual pertany una factura (`series_id`). Una sèrie és **immutable per compliment de l'AEAT** — un cop creada no es pot editar (el mètode `PUT` retorna `405`). El mode de prova usa les pròpies sèries de l'empresa sandbox i mai toca la teva numeració de producció. Consulta la secció Series a l'API Reference i [Test mode](/guides/test-mode). | | **Rectificativa (corrective invoice)** | Una factura rectificativa que corregeix una d'emesa prèviament — la manera legal d'arreglar una factura, ja que les factures emeses no es poden editar ni eliminar. Es crea mitjançant `POST /v1/invoices/{id}/corrective`; el resultat és una factura **nova** amb `is_corrective: true` i un objecte `corrective`, mapejada a un codi de tipus `R1`–`R5` de l'AEAT. El codi es deriva del slug `correction_reason` per defecte, però el pots **forçar de manera explícita** amb `correction_code` (`R1`–`R5`): una original simplificada (`F2`) només admet `R5`, i una original completa (`F1`/`F3`) només `R1`–`R4` — un codi incompatible retorna `422` amb els codis legals a `error.allowed_values`. Una `justification` opcional (`min:10`) registra la traça documental que la LIVA exigeix per a algunes causes (concurs, incobrable). Compara-la amb **anul·lar** (`POST /v1/invoices/{id}/annul`), que anul·la sense corregir. | | **Factura simplificada (simplified invoice)** | Una factura amb dades reduïdes (tipus `F2` de l'AEAT) permesa per a imports petits sota el Real Decreto 1619/2012 art. 4, sense les dades completes del destinatari. Comprova l'elegibilitat amb `POST /v1/invoices/simplified-eligibility`; agrupa'n diverses en una sola factura substitutiva completa (tipus `F3`) amb `POST /v1/invoices/substitute-simplified`. Una factura ordinària completa és de tipus `F1`. | | **Proforma** | Una factura proforma de previsualització no fiscal usada per pressupostar o sol·licitar el pagament abans d'emetre la factura real (fiscal). No porta numeració legal i es pot convertir en factura mitjançant `POST /v1/proformas/{id}/convert`. Cicle de vida: `draft`, `accepted`, `rejected`, `cancelled`, `expired`, `converted`. | | **Albarán (delivery note)** | Un document que registra les mercaderies lliurades a un client (el recurs `delivery_notes`), que més tard es pot convertir en factura. Admet una signatura manuscrita del destinatari (PNG en base64). Cicle de vida públic: `draft`, `sent`, `signed`, `invoiced`, `cancelled`. | | **`external_id` (clau d'integració)** | Un identificador de negoci extern — l'ID del registre al teu propi ERP/CRM/e-commerce — desat en un recurs per mapejar-lo i deduplicar-lo entre integracions. De format lliure (≤ 100 caràcters), únic per empresa i ortogonal als identificadors propis de Factuarea (`id`, `number`, `sku`). Cerca un registre per ell amb `POST /v1/{recurs}/find-by-external-id` (body `{ "external_id": "..." }`). Ideal com a clau de mapeig en migrar des d'una altra plataforma — consulta [Migració des de Holded](/ca/guides/migration-from-holded). | ## Compliment VeriFactu i AEAT [#compliment-verifactu-i-aeat] | Terme | Definició | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **VeriFactu** | El sistema espanyol de facturació antifrau (SIF) sota el qual cada factura emesa genera un registre "Alta" a prova de manipulacions enviat a l'AEAT. En `live` el registre es transmet a l'AEAT; en `test` es crea localment però **mai es transmet**. Es gestiona sota els endpoints `/v1/verifactu/*`. Consulta [Test mode](/guides/test-mode). | | **Huella (hash chain)** | La huella encadenada SHA-256 d'un registre VeriFactu (camp `huella`) que enllaça cada registre amb l'anterior, fent la seqüència a prova de manipulacions. Cerca un registre per ella amb `POST /v1/verifactu/records/find-by-huella`, i verifica la integritat de tota la cadena amb `GET /v1/verifactu/chain/validate`. | | **CSV (Código Seguro de Verificación)** | El **Código Seguro de Verificación** que l'AEAT retorna quan accepta un registre VeriFactu (el camp `aeat_csv`; `null` fins que s'assigna). És un codi de rebut de l'AEAT — **no** un fitxer de valors separats per comes. Cerca un registre per ell amb `POST /v1/verifactu/records/find-by-csv`. | | **FacturaE** | El format XML espanyol de factura electrònica (FacturaE 3.2.2) requerit per a facturació B2G a l'administració pública. Descarrega'l per a una factura amb `GET /v1/invoices/{id}/facturae` (signat XAdES-EPES amb certificat actiu) i envia'l a FACe via `/v1/face-submissions`. Consulta [Facturació FACe](/guides/face-invoicing). | | **FACe** | El punt general d'entrada de factures electròniques de l'administració pública espanyola (Ley 25/2013). Factuarea presenta l'XML FacturaE signat al web service de FACe i segueix l'estat de tramitació (`submitted` → `registered_rcf` → `accounted` → `paid`). Consulta [Facturació FACe](/guides/face-invoicing). | | **DIR3** | El directori espanyol d'unitats de l'administració pública. Tot client B2G porta tres codis DIR3 — oficina contable (01), órgano gestor (02) i unidad tramitadora (03) — requerits per FACe, amb format `^[A-Z][A-Z0-9]{8,9}$`. | | **Declaración responsable** | Una declaració formal de compliment (declaración responsable) que el productor del programari SIF — Factuarea — emet per acreditar la conformitat amb VeriFactu. És a nivell de productor i de només lectura (no per empresa): recupera l'actual amb `GET /v1/verifactu/declaracion-responsable`. | ## Declaracions tributàries [#tax-declarations] | Terme | Definició | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Modelo 303** | L'autoliquidació trimestral espanyola de l'IVA presentada davant l'AEAT. Genera-la amb `POST /v1/tax_reports/303`, indicant el trimestre (`1`–`4`). La resposta inclou un desglossament per tipus d'IVA (`{base, cuota}` en cèntims). Consulta la secció Tax reports a l'API Reference. | | **Modelo 347** | La declaració informativa anual que declara tercers amb qui les operacions anuals van superar el llindar legal. Genera-la amb `POST /v1/tax_reports/347`; és anual i **no** accepta un trimestre (enviar-ne un retorna un error de validació). | ## Control horari [#time-tracking] El [sistema de control horari](/guides/workforce-overview) cobreix el deure espanyol de registre de jornada. Els seus termes apareixen en noms de camp i valors d'enum dels dominis de control horari, tots darrere el mòdul `control_horario`. | Terme | Definició | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **RD-ley 8/2019** | El Reial Decret-llei 8/2019 (art. 34.9 de l'Estatut dels Treballadors), que obliga les empreses espanyoles a portar un registre diari objectiu, fiable i inalterable de la jornada de cada empleat i conservar-lo quatre anys per a la Inspecció de Treball (ITSS). Factuarea el construeix com un ledger immutable (de sola addició) segellat per una cadena de hash SHA-256 per empresa — el patró d'inviolabilitat de VeriFactu aplicat a la jornada. Consulta [Control horari](/guides/workforce-overview). | | **Fichaje (time entry)** | Cada esdeveniment de fitxatge — entrada, pausa, represa, sortida — afegit al ledger immutable (el recurs `time_entries`) i mai editat ni esborrat. L'estat de sessió en viu (`working`, `paused`, `finished`) es deriva del ledger, no es desa en una columna. Consulta [Fitxatges](/guides/time-clock). | | **Jornada (working day)** | La jornada laboral d'un empleat. Es pot partir en diversos torns (jornada partida) quan l'empleat fitxa sortida i torna a fitxar entrada el mateix dia; les hores setmanals esperades vénen de l'horari de treball assignat. | | **Registro inalterable (ledger)** | El registre horari immutable i encadenat per hash. No es pot actualitzar ni esborrar: un error es corregeix amb una sol·licitud de correcció que afegeix un assentament nou referit a l'original, de manera que tant la fallada com el seu arreglament queden al registre. Verifica'n la integritat amb `GET /v1/time-entries/chain/validate`. | | **Cierre mensual (monthly close)** | Una instantània que congela els saldos i el desglossament d'absències d'un mes finalitzat i bloqueja el període davant fitxatges retroactius (el recurs `monthly-register-closes`). Va de `closed ⇄ reopened`; la reobertura és una recuperació auditada. Consulta [Tancament mensual](/guides/monthly-time-close). | | **Sellado (seal)** | La signatura opcional i irreversible d'un tancament mensual: un digest SHA-256 canònic més una signatura RSA-SHA256 desacoblada feta amb el certificat de l'empresa, perquè un auditor pugui provar que la instantània no ha canviat des de la seva signatura. Un segellat per tancament — tornar a segellar retorna `409`. | | **Asiento de empleado (employee seat)** | La unitat de facturació del control horari. Els empleats es facturen mitjançant un add-on mensual dedicat (`employee-seats`) la quantitat del qual segueix el cens actiu; un empleat mai compta contra el límit `users` del pla. Consulta [Facturació d'assentaments d'empleat](/guides/employee-seats). | | **Tipo de ausencia (absence type)** | El que un empleat pot sol·licitar — vacances, baixa per malaltia, un dia personal — amb si és retribuïda, si requereix aprovació, i una unitat de mesura (`days` o `hours`). Cada empresa nova rep un conjunt espanyol per defecte. Consulta [Absències](/guides/absences). | | **Política de ausencia (absence policy)** | La regla que decideix quant i per a qui: una dotació (`limited` dies o `unlimited`), un mètode de meritació (`annual` o `monthly`), els tipus que cobreix i els empleats als quals s'assigna. | | **Saldo (balance)** | La dotació restant per empleat i tipus d'absència, derivada de la meritació de la política menys les sol·licituds aprovades (el recurs `absence-balances`). | | **Presencialidad (presence)** | La vista de només lectura de qui està treballant ara mateix i qui és a l'oficina o en remot avui, derivada del ledger, els horaris i el cens — mai persistida. No existeix el scope `presence:write`: declarar presència a l'oficina o en remot és una tasca només del portal. Consulta [Presència](/guides/presence). | --- # Idempotència (/ca/guides/idempotency) Les operacions d'escriptura (`POST`, `PATCH`, `DELETE`) es poden rebre diverses vegades si la connexió es talla a mitja resposta, la teva integració reintenta després d'un timeout, o hi ha reintents automàtics en un gateway intermedi. Per evitar que el mateix POST creï dues factures, l'API admet el header `Idempotency-Key`. ## Com funciona [#com-funciona] 1. El client genera una clau única per operació (un UUID v7 és l'opció recomanada, per coherència amb els identificadors de l'API). 2. Envia-la com a header en la primera petició: ```http POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b ``` 3. L'API emmagatzema el resultat (codi d'estat, headers i body) associat a aquesta clau durant **24 hores**. 4. Si arriba una nova petició amb la mateixa clau dins del TTL, l'API retorna la resposta a la cau sense tornar a executar el handler. La resposta retornada en un replay inclou el header `Idempotent-Replayed: true` perquè la puguis distingir. ## Format de la clau [#format-de-la-clau] * Una **cadena opaca** per al servidor: qualsevol valor únic és vàlid (UUID v7, UUID v4, ULID, nanoid, etc.). * Longitud entre 1 i 255 caràcters. * Recomanació: UUID v7 (`Str::uuid7()`, o qualsevol generador d'UUID v7), per coherència amb els identificadors de l'API. ```bash KEY=$(uuidgen) curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` ## Automàtic amb els SDK oficials [#automàtic-amb-els-sdk-oficials] Els [SDK de TypeScript i PHP](/sdks) adjunten un `Idempotency-Key` a cada mutació automàticament i **reutilitzen la mateixa clau en els reintents d'una crida**, de manera que una petició reintentada mai no crea per duplicat. Sobreescriu-la per crida quan vulguis deduplicació a nivell d'aplicació: ```ts // auto-generated key await factuarea.invoices.create(body); // pin your own key (e.g. your order id) await factuarea.invoices.create(body, { idempotencyKey: "order-4711" }); ``` ```php // auto-generated key $factuarea->invoices->publicApiV1InvoicesCreate($body); // pin your own key $factuarea->invoices->publicApiV1InvoicesCreate($body, idempotencyKey: 'order-4711'); ``` ## Empremta del payload [#empremta-del-payload] La clau queda lligada no només a l'`Idempotency-Key`, sinó també a una **empremta** de la petició: ``` fingerprint = sha256(method + " " + path + "\n" + canonicalize(body)) ``` On `canonicalize(body)` és el JSON amb les claus ordenades alfabèticament. Si reprodueixes la mateixa clau amb un **payload diferent**, l'API respon **409 Conflict**: ```json { "error": { "type": "idempotency_error", "code": "idempotency_key_reused", "message": "This Idempotency-Key was previously used with a different request body.", "request_id": "req_..." } } ``` Això és una protecció contra bugs: cap caller raonable canvia el body mantenint la mateixa clau. Si necessites reintentar amb dades diferents, fes servir una clau nova. ## TTL [#ttl] Les entrades es persisteixen a la taula `idempotency_keys` durant **86.400 segons (24 h)**. Després d'això, les purga un schedule diari. Si reutilitzes una clau fora de la finestra, es tracta com una de nova. ## external\_id vs Idempotency-Key [#external_id-vs-idempotency-key] Tots dos et protegeixen de duplicats, però resolen problemes diferents — i els pots fer servir junts. | | `Idempotency-Key` | `external_id` | | ----------- | ------------------------------------------------- | ------------------------------------------------------------------ | | Què és | Un header en un únic `POST`. | Una clau de negoci emmagatzemada **al recurs**. | | Vida | **Efímera** — finestra de 24 h, després es purga. | **Duradora** — permanent, mai no caduca. | | Abast | Deduplica **reintents de transport** d'una crida. | Deduplica per la teva pròpia **clau d'integració** (id d'ERP/CRM). | | Consultable | No. | **Sí** — `POST /v1/{recurs}/find-by-external-id`. | Fes servir l'**`Idempotency-Key`** perquè un reintent sigui segur: si la xarxa es talla a mitja resposta, reproduir la mateixa clau dins de 24 h retorna el resultat a la cau en lloc de crear una segona factura. Va sobre el *lliurament* d'una petició. Fes servir **`external_id`** per vincular un recurs de Factuarea amb un registre del teu propi sistema (un id de comanda, un número de document d'ERP). Envia'l al body de creació i l'API garanteix que és únic per empresa (`UNIQUE(company_id, external_id)`). Més tard pots localitzar el recurs per aquesta clau, sense emmagatzemar l'`id` de Factuarea: ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/find-by-external-id \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "ORDER-4711" }' | jq '.data.id' ``` En resum: `Idempotency-Key` és una protecció de reintent de vida curta; `external_id` és el teu enllaç permanent i consultable. Una integració típica configura **tots dos** — una clau nova per intent i un `external_id` estable per objecte de negoci. ## Recomanacions per endpoint [#recomanacions-per-endpoint] | Endpoint | Idempotència recomanada | | ------------------------------------------ | --------------------------------------------------------- | | `POST /v1/invoices` | **Sí** (crítica) | | `POST /v1/quotes` | **Sí** | | `POST /v1/clients` | **Sí** | | `POST /v1/invoices/{id}/send` | Sí | | `POST /v1/invoices/{id}/mark-paid` | Sí | | `POST /v1/invoices/{id}/payments` | **Sí** (un pagament reintentat es comptaria dues vegades) | | `POST /v1/purchase_invoices/{id}/payments` | **Sí** (un pagament reintentat es comptaria dues vegades) | | `GET /v1/...` | N/A (sense efecte) | | `PATCH /v1/...` | Opcional (PATCH és idempotent per definició) | | `DELETE /v1/...` | Opcional | Stripe documenta el mateix patró: si vens d'allà, el contracte és idèntic. ## Exemple de reintent segur [#exemple-de-reintent-segur] ```python import os, uuid, requests from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, max=10), retry=retry_if_exception_type(requests.exceptions.RequestException), ) def create_invoice(payload): key = str(uuid.uuid4()) return requests.post( 'https://api.factuarea.com/v1/invoices', json=payload, headers={ 'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}", 'Idempotency-Key': key, }, timeout=30, ) ``` ```javascript async function createInvoiceWithRetry(payload, maxAttempts = 5) { const key = crypto.randomUUID(); let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const res = await fetch('https://api.factuarea.com/v1/invoices', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (res.ok) return res.json(); if (res.status >= 500) { await new Promise(r => setTimeout(r, 2 ** attempt * 100)); continue; } return res.json(); } catch (err) { lastError = err; await new Promise(r => setTimeout(r, 2 ** attempt * 100)); } } throw lastError; } ``` **Important**: la clau ha de romandre **constant en tots els reintents del mateix POST**. Si generes una clau nova en cada reintent, perds la protecció. En l'exemple de `tenacity` en Python, la `key` es genera fora del closure i es reutilitza en tots els reintents. ## Què NO és la idempotència [#què-no-és-la-idempotència] * **No** és el mateix que el límit de peticions: una clau idempotent reproduïda dins del TTL **no compta** contra la teva quota; però claus diferents amb el mateix payload sí que compten, una a una. * **No** substitueix un lock distribuït per la teva banda. Si dos workers creen factures de manera concurrent amb claus diferents, totes dues es persistiran; generar la clau correctament (p. ex. derivada del teu propi ID) és responsabilitat teva. * **No afecta** les respostes `4xx` pròpies del servidor: si la primera petició va respondre `422 invalid_request_error`, aquest 422 es desa a la cau. Reproduir la clau retorna el mateix 422 amb `Idempotent-Replayed: true`. --- # Clients internacionals (/ca/guides/international-customers) Facturar fora d'Espanya planteja dues preguntes que el cas interior no planteja mai: **com identifiques un destinatari que no té NIF espanyol**, i **què rep l'AEAT per una operació que és exempta, amb inversió del subjecte passiu o localitzada a l'estranger**. Són independents, i aquesta pàgina les respon en aquest ordre. ## Quan aplica [#when] Sempre que el destinatari no sigui un contribuent espanyol, o l'operació estigui localitzada fora del territori peninsular d'aplicació de l'IVA. La identificació és una propietat del **client**; la qualificació és una propietat de l'**operació**, i el mateix client pot aparèixer en operacions de tipus diferents. ## Identificar el client [#identity] Un client no espanyol s'identifica amb `alternative_id`, un objecte de `{type, value, country_code}` que és **mútuament excloent amb el `tax_id` espanyol** ([`BR-CLI-017`](#traceability)). El tipus pertany al catàleg d'identificació de l'AEAT, llista L7, i cada cas té el seu propi codi numèric, que viatja a la cadena VeriFactu: | `type` | Codi AEAT | Significat | | ----------------------- | --------- | -------------------------------------------------------- | | `nif_iva` | 02 | Número d'operador intracomunitari (NIF-IVA). | | `passport` | 03 | Passaport. | | `country_id` | 04 | Document oficial d'identificació del país de residència. | | `residence_certificate` | 05 | Certificat de residència fiscal. | | `other_document` | 06 | Altre document probatori. | | `not_registered` | 07 | No inscrit al cens de l'AEAT (*No censado*). | La **matriu de tipus i país** és una invariant dura, no un suggeriment: `nif_iva` només és legal per a països de la UE, perquè *és* el número d'operador intracomunitari; els altres tipus valen per a qualsevol país que no sigui Espanya; i `country_code: "ES"` es rebutja sempre, perquè Espanya fa servir `tax_id`. Una combinació il·legal respon `422`: ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "name": "Müller GmbH", "alternative_id": { "type": "nif_iva", "value": "DE811569869", "country_code": "DE" } }' ``` Els valors heretats `tax_id_foreign` i `national_id` encara s'accepten perquè les integracions existents no es trenquin. La normalització té en compte el país: `national_id` esdevé `country_id` incondicionalment, mentre que `tax_id_foreign` esdevé `nif_iva` per a un país de la UE i `other_document` altrament — perquè un `tax_id_foreign` de fora de la UE no pot ser un número intracomunitari, i la matriu el rebutjaria. Si no envies `alternative_id` en absolut —un client estranger amb només un país i un identificador fiscal—, la cadena VeriFactu cau al tipus d'identificació `02`, el cas intracomunitari més habitual. Enviar el camp explícitament és estrictament millor. ### `vat_id` és text lliure, i no es verifica [#vat-id] El camp del número d'IVA intracomunitari accepta qualsevol cadena de fins a 20 caràcters. **No es valida contra el registre VIES**, no se'n comprova el format per país, i no es contrasta amb `tax_id` ([`BR-CLI-003`](#traceability)). Un prefix de país equivocat s'accepta. Un client que hauria d'estar sota el règim intracomunitari però que no té `vat_id` ni es bloqueja ni es marca. `vat_id` i `tax_id` són camps separats que conviuen: una empresa espanyola pot portar un NIF nacional i el mateix número amb el prefix de país com a número d'IVA intracomunitari. ### Verificar un destinatari espanyol abans de facturar [#census] Per als destinataris que **sí** que tenen NIF espanyol, [`POST /v1/clients/census-verification`](/api-reference/clients/public-api.v1.clients.verify_census) (scope `clients:read`) comprova el parell de nom i NIF contra el cens de l'AEAT abans que facturis, anticipant el rebuig VeriFactu més freqüent — el del destinatari que el cens no identifica ([`BR-CLI-015`](#traceability)). És deliberadament informativa: no bloqueja mai desar un client ni emetre una factura, no persisteix res, i és **fail-open** — una AEAT inaccessible respon `200` amb un estat de no disponible, mai un `5xx`. Està limitada per freqüència, perquè pot arribar a la xarxa de l'AEAT. Vegeu [Verificació censal](/guides/census-verification) per al flux complet. ## El mapa d'escenaris [#map] Aquest és el mapa de l'escenari de negoci al que rep l'AEAT ([`BR-VFC-029`](#traceability)): | Escenari | Règim d'operació de capçalera | Què arriba a l'AEAT | | -------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------ | | Lliurament intracomunitari de **béns** | `intracomunitaria` | `E5` — subjecta i exempta, art. 25 LIVA | | **Serveis** amb inversió del subjecte passiu | `isp` | `S2` — subjecta i **no** exempta, quota repercutida `0` (l'autorepercuteix el destinatari) | | **Exportació** fora de la UE | `importacion_exportacion` | `E2` — subjecta i exempta, art. 21 LIVA | | Vendes a distància per **finestreta única** | (general) | `regime_key: 17` — Capítol XI del Títol IX, OSS i IOSS | **La inversió del subjecte passiu no és una exempció.** És una *qualificació* derivada del règim de capçalera — `S2`, subjecta i no exempta, amb la quota repercutida forçada a zero perquè és el destinatari qui liquida l'impost. **No** és una causa d'exempció de línia, i en particular **no** és `E4`: aquest codi és l'exempció dels arts. 23 i 24 LIVA, per a dipòsits duaners i règims suspensius, que és una cosa completament diferent. Una factura que declara la inversió del subjecte passiu com a operació exempta declara malament tant la qualificació com la quota. Les quatre qualificacions assolibles des del règim de capçalera són `S1` (general), `S2` (inversió del subjecte passiu), `E5` (intracomunitària) i `E2` (importació o exportació). Els altres codis d'exempció —`E1`, `E3`, `E4`, `E6`— existeixen al catàleg de l'AEAT però només s'assoleixen com a causa d'exempció de **línia**. ## Què envia l'API [#api] Aquí ve la part que decideix com construeixes el payload, i és una restricció real i no una preferència d'estil. **El règim d'operació de capçalera és de només lectura a la v1.** Ni [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) ni [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update) no accepten `operation_regime`; l'objecte factura el retorna, i tota factura creada per l'API pública neix sota el règim general. La causa d'exempció a nivell de document és de només lectura pel mateix motiu. El `preferred_operation_regime` del client —acceptat a [`POST /v1/clients`](/api-reference/clients/public-api.v1.clients.create) amb els valors `general`, `intracomunitaria`, `importacion_exportacion` i `isp`— es desa i es retorna, però **no** fixa el règim de les factures que crees. És una preferència declarativa per al teu propi ús. El que *sí* que pots expressar per línia és la causa d'exempció. Així: | Escenari | Com ho expresses a la v1 | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Lliurament intracomunitari de béns | `tax_rate: 0` + `exemption_reason: "E5"` per línia. | | Exportació fora de la UE | `tax_rate: 0` + `exemption_reason: "E2"`, normalment amb `regime_key: "02"`. | | Vendes a distància per finestreta única | `regime_key: "17"` per línia, amb el tipus del país de destinació. | | **Inversió del subjecte passiu** | **No expressable.** `S2` deriva del règim de capçalera, i el catàleg de línia no conté codis `S` per disseny. | Aquesta última fila és la resposta honesta, i té conseqüències: una factura amb inversió del subjecte passiu creada per l'API pública quedarà qualificada com a `S1` i amb quota repercutida, que no és el que vols dir. Fins que el règim de capçalera no sigui escrivible, emet aquestes factures des del tauler. Queda recollit a [Abast i limitacions](/guides/scope-and-limitations). L'exemple publicat `intracomunitario_bienes` de l'operació de creació té exactament aquesta forma —tipus zero, més `E5`, més una clau de règim explícita— en lloc d'un règim de capçalera que no podria fixar: ```json { "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "notes": "Entrega intracomunitaria de bienes exenta (art. 25 LIVA)", "lines": [ { "description": "Suministro de maquinaria a cliente UE (DE)", "quantity": 1, "unit_price": 5000, "tax_rate": 0, "exemption_reason": "E5", "regime_key": "01" } ] } ``` Una factura simplificada no és mai una opció per a cap d'aquests escenaris: la comprovació d'admissibilitat bloqueja les operacions intracomunitàries, la inversió del subjecte passiu i qualsevol destinatari fora d'Espanya abans fins i tot de considerar l'import. Vegeu [Factures simplificades o completes](/guides/simplified-vs-full-invoices#when). ## Què surt al PDF [#pdf] El bloc de destinatari imprimeix la identificació alternativa exactament tal com s'ha aportat, congelada en el moment d'emetre com la resta del *snapshot* de destinatari ([`BR-INV-024`](#traceability)). La menció legal —art. 25 LIVA en un lliurament intracomunitari, art. 21 en una operació amb tercers països, art. 84.Uno.2 en la inversió del subjecte passiu— deriva del règim de **capçalera**, i per tant no apareix automàticament en una factura creada per la v1 ([`BR-TAX-024`](#traceability)). Dues opcions: posa el text a `notes`, o fes servir l'`exemption_reason_text` de línia, que s'imprimeix sota la descripció de la línia i és només de presentació. ## Què arriba a l'AEAT [#aeat] **Al registre VeriFactu**, el tipus d'identificació del destinatari viatja com el codi AEAT de la taula L7 de dalt, i el desglossament porta la qualificació descrita a [El mapa d'escenaris](#map) — codis d'operació exempta per a `E5` i `E2`, i `S2` amb quota zero en la inversió del subjecte passiu. **A la declaració anual d'operacions amb terceres persones** (**Modelo 347**), les operacions intracomunitàries i les importacions o exportacions queden **excloses** ([`BR-TXR-022`](#traceability)): es declaren per les seves pròpies vies —la declaració recapitulativa per a les operacions intracomunitàries, i la documentació duanera per a la resta— i declarar-les dues vegades produiria un desquadrament a la declaració creuada. La inversió del subjecte passiu es comporta al revés: és una operació **interior** i sí que apareix en aquella declaració. La classificació fa servir el règim de **capçalera** de la factura, així que una factura mixta es classifica sencera. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-CLI-003` — `vat_id` com a text lliure, sense validació VIES, independent de `tax_id`. * `BR-CLI-015` — verificació censal del destinatari: informativa, *fail-open* i sense estat. * `BR-CLI-017` — el catàleg d'identificació alternativa L7 de l'AEAT, la matriu de tipus i país i els àlies heretats acceptats. * `BR-INV-024` — el *snapshot* immutable del destinatari. * `BR-INV-031` — el catàleg tancat de claus de règim usat per a les línies de finestreta única i d'exportació. * `BR-INV-032` — les causes d'exempció de línia i el seu valor derivat de la capçalera. * `BR-TAX-024` — la causa d'exempció a nivell de document i la seva menció legal automàtica. * `BR-VFC-029` — el mapa de qualificacions: `S1`, `S2`, `E5` i `E2` derivats del règim de capçalera, i la inversió del subjecte passiu com a qualificació i no com a exempció. * `BR-TXR-022` — exclusió de les operacions intracomunitàries i d'importació o exportació de la declaració anual d'operacions amb tercers, i la inclusió de la inversió del subjecte passiu interior. --- # Classificació fiscal i exempcions per línia (/ca/guides/line-tax-classification-and-exemptions) Una línia de factura porta més informació fiscal que un tipus impositiu. Quatre camps opcionals decideixen com es classifica l'operació, si es repercuteix IVA o no, i quant paga realment el destinatari: | Camp | Què fa | | ------------------ | ------------------------------------------------------------------------------- | | `exemption_reason` | Declara la línia exempta (`E1`–`E6`) o no subjecta (`N1`, `N2`). | | `regime_key` | Declara el règim especial — vegeu [Claus de règim](/guides/regime-keys). | | `retention_rate` | Retenció d'IRPF, **restada** de l'import a pagar. | | `surcharge_rate` | Recàrrec d'equivalència, sumat — i només en combinacions aparellades legalment. | Tots quatre són opcionals i additius. Una factura que els omet tots es comporta exactament igual que abans que existissin, *huella* inclosa. ## Quan aplica [#when] Declara una causa d'exempció quan l'operació estigui exempta o no subjecta segons la Llei de l'IVA. Declara retenció quan facturis com a professional o arrendis un local de negoci. Declara recàrrec quan el teu client sigui un minorista en règim de recàrrec d'equivalència. La distinció entre les dues famílies de codis és legal, no cosmètica ([`BR-INV-032`](#traceability)): | Família | Codis | Base a la LIVA | Desglossament AEAT | | --------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------ | | **Exempta** | `E1` art. 20 · `E2` art. 21 · `E3` art. 22 · `E4` arts. 23 i 24 · `E5` art. 25 · `E6` altres | L'operació *sí* que està subjecta a l'IVA, i exempta. | Declara un codi d'operació exempta. Sense quota d'IVA. | | **No subjecta** | `N1` arts. 7, 14 i altres · `N2` regles de localització | L'operació queda fora de l'àmbit de l'impost. | Declara una qualificació de no subjecció. | El catàleg no conté **cap codi `S`** a propòsit. «Subjecta i no exempta» és el valor per defecte, no una causa seleccionable, i **la inversió del subjecte passiu es modela a la capçalera de la factura**, no per línia. Com que el règim de capçalera és de només lectura a la v1, la inversió del subjecte passiu no es pot declarar per l'API pública — vegeu [Clients internacionals](/guides/international-customers#map). ## Què envia l'API [#api] ### Exempció i no subjecció [#exemption] `lines[].exemption_reason` a [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) i [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). Un valor fora del catàleg de vuit codis respon `422` amb `allowed_values`. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Exportación de maquinaria", "quantity": 1, "unit_price": 100, "tax_rate": 0, "exemption_reason": "E2" }, { "description": "Servicio de instalación", "quantity": 1, "unit_price": 50, "tax_rate": 21 } ] }' ``` El desglossament de l'AEAT agrupa pel parell **(tipus impositiu, causa d'exempció)**, així que una factura mixta produeix un grup per combinació i cada grup quadra pel seu compte. Les línies que comparteixen tots dos valors s'agreguen en un sol grup. Una línia que omet el camp cau a la qualificació derivada de la capçalera de la factura. Com que una factura creada per la v1 té sempre el règim general de capçalera, aquest valor per defecte és «subjecta i no exempta» — i per això una línia exempta ho ha de dir de manera explícita. ### La retenció d'IRPF resta [#irpf] `lines[].retention_rate` és un percentatge de 0 a 100, opcionalment acompanyat de `lines[].retention_rate_id`, una referència a una retenció del teu catàleg. La fórmula canònica del total és: ``` total = subtotal + IVA − retenció + recàrrec d'equivalència ``` La retenció és diners que el client es queda i ingressa a l'Administració tributària en nom del professional, així que **redueix** l'import a pagar ([`BR-INV-033`](#traceability)): ```json { "lines": [ { "description": "Servicios de consultoría", "quantity": 1, "unit_price": 1000, "tax_rate": 21, "retention_rate": 15 } ] } ``` Aquella línia factura 1000, repercuteix 210 d'IVA, reté 150, i el client paga 1060\. Si envies **alhora** `retention_rate` i `retention_rate_id`, han de coincidir. Una discrepància és un `422` que anomena tots dos percentatges, en lloc d'una decisió silenciosa sobre quin guanya. Alguns tipus de retenció s'emmagatzemen amb signe negatiu — una convenció visual heretada que significa «això es reté». El càlcul pren el valor absolut i la resta està cablejada a la fórmula mateixa, així que el signe no canvia mai el resultat ([`BR-TAX-008`](#traceability)). El catàleg fiscal públic publica sempre aquests tipus en **positiu**. ### La matriu del recàrrec d'equivalència és tancada [#surcharge] `lines[].surcharge_rate` no és un número lliure. Tota línia amb recàrrec per damunt de zero es valida contra l'aparellament legal amb el seu tipus d'IVA ([`BR-INV-034`](#traceability)): | Tipus d'IVA | Recàrrec legal | | ----------- | -------------- | | 21 % | 5,2 % | | 10 % | 1,4 % | | 4 % | 0,5 % | | 0 % | 0 % | Una combinació il·legal —21 % d'IVA amb un recàrrec de l'1,4 %, per exemple— respon `422` amb els parells legals a `allowed_values`. La comparació és per valor arrodonit a dos decimals, així que `5.2` i `5.20` són el mateix parell. Les operacions sota aquest règim solen portar a més `regime_key: "18"`. ### Què retorna cada línia [#line-output] L'objecte línia de factura retorna `tax_rate`, `retention_rate`, `surcharge_rate`, `discount_percent`, el `subtotal` calculat, `taxes` i `total`, més els camps fiscals: `regime_key`, `exemption_reason`, `indirect_tax_regime` i `aeat_tax_code`. Els dos últims són un ***snapshot* fiscal congelat**, escrit en construir la línia i mai recalculat ([`BR-TAX-023`](#traceability)). Una factura emesa no canvia el seu règim indirecte perquè l'empresa traslladi després el seu domicili fiscal, i les línies històriques anteriors al *snapshot* es queden buides en lloc de reomplir-se amb les dades d'avui. ### D'on surten els valors per defecte [#defaults] Quan omets un tipus, el resol una única cadena del backend compartida per totes les superfícies —tauler, API pública, eines d'agent, importadors, factures recurrents— en ordre estricte de prioritat ([`BR-TAX-025`](#traceability)): **Valors per defecte del client.** El client desa *tipus*, no referències, i cada tipus es resol a un impost concret **filtrat pel règim indirecte de l'emissor**: un 7 % per defecte d'un client en una empresa canària resol a IGIC al 7 %, no a un IVA peninsular. **Ajustos de l'empresa**, inclosa la suggerència derivada de la zona AEAT de l'empresa. **El catàleg global.** La cadena és de millor esforç i **no retorna mai cap error** per un valor per defecte irresoluble: degrada a l'esglaó següent. Si el client està marcat com a subjecte al recàrrec d'equivalència i el tipus d'IVA resolt té un recàrrec legalment vinculat, aquest recàrrec s'injecta als valors per defecte ([`BR-TAX-022`](#traceability)). Consulta-la directament amb [`GET /v1/taxes/defaults/{docType}`](/api-reference/taxes/public-api.v1.taxes.defaults) quan vulguis ensenyar als teus usuaris el que s'aplicarà abans que ho confirmin. ## Què surt al PDF [#pdf] Canvien dues coses al document imprès. **El bloc de totals** reflecteix la fórmula de dalt: la retenció apareix com a resta i el recàrrec d'equivalència com a suma, així que l'import a pagar difereix de `subtotal + IVA`. **Les mencions legals.** Quan la factura porta una causa d'exempció a nivell de document, la seva frase legal —citant l'article de la LIVA— s'afegeix com a primera menció legal de la factura ([`BR-TAX-024`](#traceability)). Aquesta causa és un camp de **capçalera**, un per factura, i és de **només lectura per l'API pública**: l'objecte factura exposa `exemption_reason` i `legal_mentions`, però cap operació de la v1 no els fixa. Una factura creada per la v1 no imprimeix, per tant, cap frase automàtica d'exempció; posa el text a `notes` si el document ho necessita. El camp de línia `exemption_reason_text` (fins a 255 caràcters) existeix amb el mateix propòsit a nivell de línia, i és només de presentació — no té cap efecte fiscal. ## Què arriba a l'AEAT [#aeat] Una qualificació per grup de desglossament. Una línia que declara un codi `E` produeix una entrada d'**operació exempta** amb aquest codi literal i sense quota repercutida; una línia que declara un codi `N` produeix una qualificació de **no subjecció**. Una línia que no declara res hereta la qualificació derivada de la capçalera ([`BR-VFC-029`](#traceability)). La clau d'agrupació és el parell (tipus impositiu, causa d'exempció), que és el que permet a una factura mixta passar la validació de l'AEAT: cada grup declara la seva pròpia base, el seu propi tipus i la seva pròpia quota, i dins del grup es compleix `base × tipus = quota`. La retenció **no** apareix al desglossament VeriFactu — no és IVA. Es declara a les declaracions de retencions i redueix el total de la factura. El recàrrec d'equivalència només es propaga a les línies subjectes i no exemptes; les línies exemptes no porten ni IVA ni recàrrec. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-INV-032` — el catàleg tancat `E1`–`E6` / `N1`–`N2`, el valor derivat de la capçalera, l'agrupació per (tipus, causa) i la invariant de *huella* idèntica. * `BR-INV-033` — la retenció d'IRPF per línia al contracte v1 i la comprovació de coherència entre el tipus i l'impost referenciat. * `BR-INV-034` — la matriu legal tancada de parells d'IVA i recàrrec. * `BR-TAX-008` — la retenció emmagatzemada amb signe però calculada en valor absolut. * `BR-TAX-022` — el vincle legal d'un tipus d'IVA amb el seu recàrrec d'equivalència. * `BR-TAX-023` — el *snapshot* fiscal immutable per línia. * `BR-TAX-024` — la causa d'exempció a nivell de document i la menció legal automàtica. * `BR-TAX-025` — la cadena client → empresa → catàleg global de valors fiscals per defecte. * `BR-VFC-029` — com es deriva la qualificació quan la línia no declara cap causa. --- # Migració des de Holded (/ca/guides/migration-from-holded) Aquesta guia documenta la migració des de l'API de Holded (un dels principals competidors en el sector del SaaS de facturació espanyol) a la Public API v1 de Factuarea. Cobreix el mapeig de recursos, les diferències de nomenclatura, els endpoints equivalents i un script d'exemple en Python que migra una empresa sencera. ## Mapeig de recursos [#mapeig-de-recursos] | Holded | Factuarea | Notes | | --------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contacts` | `clients` + `suppliers` | Holded els barreja a `contacts` amb un camp `type`. Factuarea els separa en dos endpoints diferents. | | `products` | `products` | Nomenclatura idèntica. | | `documents/invoice` | `invoices` | Endpoint dedicat. | | `documents/estimate` | `quotes` | Canvi de nom: Holded fa servir "estimate", Factuarea "quote". | | `documents/proform` | `proformas` | Reanomenat a "proforma" sense abreujar. | | `documents/waybill` | `delivery_notes` | Nomenclatura canònica espanyola/legal. | | `documents/purchase` | `purchase_invoices` | | | `documents/recurring` | `recurring_invoices` | | | `taxes` | `taxes` | Mateix concepte. | | `numerations` | `series` | Holded "numeration", Factuarea "series". El `format` de Holded es mapeja a `number_format`, una màscara de numeració configurable (padding + token d'any + separador), p. ex. `{code}-{YYYY}-{000}`. | | `tags` | `tags` | Etiquetes de classificació lliure en un document (slugs en minúscula, ≤ 40 caràcters, ≤ 30 per document). | | custom fields | `custom_fields` | Metadades d'integració tipades `[{field, value}]` en un document (≤ 50 entrades). | | `webhooks` | `webhook_endpoints` (+ anidat `deliveries`) | Factuarea separa la configuració de l'endpoint de la traçabilitat de lliuraments (`GET /v1/webhook_endpoints/{id}/deliveries`). | ## Diferències clau [#diferències-clau] ### 1. Autenticació [#1-autenticació] * Holded: header `key: `. * Factuarea: `Authorization: Bearer fact_live_...` o `X-API-Key: fact_live_...`. OpenAPI estàndard. ### 2. Identificadors [#2-identificadors] * Holded: IDs opacs de tipus string-numèric. * Factuarea: cada recurs té una key `id` el valor de la qual és un **UUID v7** (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b`) — codifica un timestamp i és ordenable lexicogràficament. Les foreign keys fan servir `*_id` (p. ex. `client_id`). **Desa l'ID de Holded a `external_id` — és l'estratègia de migració recomanada.** Cada recurs de Factuarea accepta un `external_id` (una clau d'integració externa, ≤ 100 caràcters, única per empresa, diferent del `tax_id` fiscal). Escriu l'ID original de Holded a dins en cada creació. Això fa la migració **nativament idempotent**: no cal mantenir una taula de mapeig `holded_id ↔ factuarea_id` — per trobar el registre de Factuarea d'un ID de Holded, crida `POST /v1/{recurs}/find-by-external-id` amb el body `{ "external_id": "" }`. Disponible a `clients`, `suppliers`, `products`, `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices` i `recurring_invoices`. Consulta [external\_id al glossari](/ca/guides/glossary). ### 3. Paginació [#3-paginació] * Holded: `?starttmp=...&endtmp=...` (timestamps a la URL). * Factuarea: paginació per cursor (`starting_after`, `ending_before`) pel `id` del recurs. Consulta [Paginació](/guides/pagination). ### 4. Errors [#4-errors] * Holded: status code + array `errors` o string `error`. * Factuarea: embolcall `{ error: { type, code, message, request_id, doc_url } }`. Consulta [Errors](/guides/errors). ### 5. Webhooks [#5-webhooks] * Holded: payload sense signar (validació basada en IP). * Factuarea: signatura HMAC SHA256 obligatòria, tolerància de ±5min, reintents exponencials fins a 8 intents. Consulta [Webhooks](/guides/webhooks). ### 6. Idempotència [#6-idempotència] * Holded: no suportada. * Factuarea: header `Idempotency-Key` amb TTL de 24h. Consulta [Idempotència](/guides/idempotency). ## Endpoints equivalents (operacions més comunes) [#endpoints-equivalents-operacions-més-comunes] | Operació | Holded | Factuarea | | ------------------------------- | ---------------------------------------------------- | ---------------------------------- | | Llistar factures | `GET /invoicing/v1/documents/invoice` | `GET /v1/invoices` | | Crear factura | `POST /invoicing/v1/documents/invoice` | `POST /v1/invoices` | | Marcar factura com a pagada | `POST /invoicing/v1/documents/invoice/{id}/pay` | `POST /v1/invoices/{id}/mark-paid` | | Enviar factura per email | `POST /invoicing/v1/documents/invoice/{id}/send` | `POST /v1/invoices/{id}/send` | | Descarregar PDF | `GET /invoicing/v1/documents/invoice/{id}/pdf` | `GET /v1/invoices/{id}/pdf` | | Llistar clients | `GET /invoicing/v1/contacts?type=client` | `GET /v1/clients` | | Crear client | `POST /invoicing/v1/contacts` (amb `type=client`) | `POST /v1/clients` | | Convertir pressupost en factura | `POST /invoicing/v1/documents/estimate/{id}/convert` | `POST /v1/quotes/{id}/convert` | | Crear webhook | `POST /invoicing/v1/webhooks` | `POST /v1/webhook_endpoints` | ## Diferències de payload [#diferències-de-payload] ### Crear factura [#crear-factura] Holded: ```json POST /invoicing/v1/documents/invoice { "contactId": "5e1c2a3b4f5d6e7f8a9b0c1d", "date": 1747314060, "items": [ { "name": "Service", "units": 1, "subtotal": 99.00, "tax": 21 } ] } ``` Factuarea: ```json POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b { "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "lines": [ { "description": "Service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] } ``` Canvis: * `contactId` → `client_id` (FK explícita; el valor és un UUID v7). * `date` (timestamp) → `issued_on` (`YYYY-MM-DD`), amb `due_on` obligatori. * `items[].subtotal` (import) → `lines[].unit_price` (preu unitari; l'API calcula els totals). * `items[].tax` (percentatge en línia) → `lines[].tax_rate_id` (FK al catàleg d'impostos). * `series_id` obligatori — Factuarea exigeix configurar la sèrie abans d'emetre (coherència amb l'AEAT). ### Webhooks: signatura [#webhooks-signatura] Holded no signa. Factuarea sí (HMAC SHA256). Després de migrar **has de** validar la signatura al teu handler. Consulta [Webhooks](/guides/webhooks). ## Script mínim de migració (Python) [#script-mínim-de-migració-python] Aquest script és il·lustratiu, no llest per a producció. Prova'l en staging i valida les dades migrades manualment abans d'executar-lo contra producció. ```python """ Migrate contacts and products from Holded to Factuarea. Requires: pip install requests tenacity python-dotenv """ import os, time, uuid, requests from tenacity import retry, stop_after_attempt, wait_exponential HOLDED_API = 'https://api.holded.com/api/invoicing/v1' FACTUAREA_API = 'https://api.factuarea.com/v1' HOLDED_HEADERS = {'key': os.environ['HOLDED_KEY']} FACTUAREA_HEADERS = { 'Authorization': f"Bearer {os.environ['FACTUAREA_KEY']}", 'Content-Type': 'application/json', } @retry(stop=stop_after_attempt(5), wait=wait_exponential(max=10)) def fact_post(path, payload, key=None): headers = dict(FACTUAREA_HEADERS) headers['Idempotency-Key'] = key or str(uuid.uuid4()) r = requests.post(f"{FACTUAREA_API}{path}", json=payload, headers=headers, timeout=30) if r.status_code >= 500: r.raise_for_status() return r def fetch_holded_contacts(): url = f"{HOLDED_API}/contacts" while url: r = requests.get(url, headers=HOLDED_HEADERS, timeout=30) r.raise_for_status() payload = r.json() for c in payload.get('contacts', payload if isinstance(payload, list) else []): yield c url = payload.get('next') if isinstance(payload, dict) else None def migrate_clients(): migrated = 0 for h in fetch_holded_contacts(): if h.get('type') != 'client': continue payload = { 'name': h['name'], 'tax_id': h.get('code') or h.get('vatnumber'), 'email': h.get('email'), 'phone': h.get('phone'), 'address': h.get('billAddress', {}).get('address'), 'postal_code': h.get('billAddress', {}).get('postalCode'), 'city': h.get('billAddress', {}).get('city'), 'province': h.get('billAddress', {}).get('province'), 'country': h.get('billAddress', {}).get('country', 'ES'), 'external_id': h['id'], # Holded ID → external_id: the mapping key. Reconcile later via POST /v1/clients/find-by-external-id } idem_key = f"migrate-client-{h['id']}" # deterministic for retry-safety r = fact_post('/clients', {k: v for k, v in payload.items() if v is not None}, key=idem_key) if r.status_code == 201: migrated += 1 elif r.status_code == 409: # already exists (another migration run) pass else: print(f" ERROR {r.status_code} for {h['id']}: {r.text[:200]}") time.sleep(0.1) # courtesy with rate limits print(f"Clients migrated: {migrated}") if __name__ == '__main__': migrate_clients() ``` ## Importació massiva de clients des de l'export de Holded [#client-import] L'script de dalt crea els clients d'un en un amb `POST /v1/clients`. Si prefereixes ficar-hi directament el **fitxer d'export de contactes** de Holded, fes servir `POST /v1/clients/import` (scope `clients:write`, `multipart/form-data`) amb el preset de mapeig de sota. Rep un `file` (CSV, XLSX, XLS, ODS o TXT, fins a 10 MB), un objecte `mapping` i un flag `dry_run`. ### El preset de mapeig [#client-import-mapping] Al `mapping`, la **clau és la capçalera de columna tal com apareix al teu fitxer** i el **valor és el camp destí**. Holded tradueix les capçaleres de l'export a l'idioma del compte, així que obre la primera línia del teu fitxer i ajusta les claus — els valors de la dreta no canvien mai: ```json { "Name": "name", "Trade name": "commercial_name", "VAT number": "tax_id", "EU VAT number": "vat_id", "Email": "email", "Phone": "phone", "Mobile": "mobile", "Fax": "fax", "Website": "website", "Address": "address", "City": "city", "Postal code": "postal_code", "Province": "province", "Country": "country", "IBAN": "bank_iban", "Contact person": "contact_person", "Notes": "notes" } ``` Tres regles que l'API imposa sobre el mapeig mateix: * **`name` i `tax_id` són destins obligatoris.** Un mapeig sense tots dos es rebutja amb `422` abans de llegir ni una sola fila. * **Cap destí dues vegades.** Dues capçaleres apuntant al mateix camp és un error, no un «guanya l'última» silenciós. * **Les columnes sense mapejar s'ignoren.** La columna `Id` de Holded n'és una — mira el [pas 3](#client-import-reconcile). ### Camps destí [#client-import-fields] Aquests són els camps que accepta l'importador de clients. Un destí fora d'aquesta taula s'**ignora en silenci**: ni s'escriu ni es reporta com a error, exactament igual que si la columna no s'hagués mapejat. Revisa el teu `mapping` contra aquesta taula abans de l'execució real — una errada com `"e-mail"` et costa la columna sencera a totes les files, i la importació continua responent `200`. | Destí | Obligatori | Es valida com | | ------------------------ | ---------- | ------------------------------------------------------------------ | | `name` | **sí** | no buit | | `tax_id` | **sí** | NIF/CIF/NIE espanyol | | `commercial_name` | no | text lliure | | `vat_id` | no | text lliure | | `email` | no | adreça de correu | | `phone` | no | telèfon | | `mobile` | no | telèfon | | `fax` | no | text lliure | | `website` | no | text lliure | | `address` | no | text lliure | | `address_line2` | no | text lliure | | `address_number` | no | text lliure | | `address_floor` | no | text lliure | | `address_door` | no | text lliure | | `address_staircase` | no | text lliure | | `city` | no | text lliure | | `postal_code` | no | text lliure | | `province` | no | text lliure | | `country` | no | text lliure | | `bank_iban` | no | text lliure — passa a ser el compte bancari per defecte del client | | `default_vat_rate` | no | numèric | | `default_retention_rate` | no | numèric | | `default_discount` | no | numèric | | `payment_method` | no | text lliure | | `payment_terms_days` | no | numèric | | `contact_person` | no | text lliure | | `notes` | no | text lliure | Els decimals admeten tant `.` com `,` de separador. El `tax_id` es desa en majúscules, així que busca'l en majúscules després. Els camps que l'importador **no** cobreix — `external_id`, `billing_emails`, `alternative_id`, `metadata`, els codis DIR3 i el flag de recàrrec d'equivalència — només es poden fixar amb `POST /v1/clients`, `POST /v1/clients/bulk-create` o `PUT /v1/clients/{id}`. ### Pas 1 — execució en sec [#client-import-dry-run] Comença sempre amb `dry_run: true`. No s'escriu res, no es consumeix quota mensual de files, i obtens el veredicte fila a fila: ```bash curl -X POST https://api.factuarea.com/v1/clients/import \ -H "Authorization: Bearer fact_live_..." \ -F "file=@holded-contactes.csv" \ -F 'mapping={"Name":"name","VAT number":"tax_id","Email":"email"}' \ -F "dry_run=true" ``` ```json { "data": { "object": "client_import_preview", "total_rows": 128, "rows": [ { "row": 2, "status": "valid", "errors": [], "warnings": [] }, { "row": 3, "status": "error", "errors": [ { "param": "tax_id", "code": "INVALID_FORMAT", "message": "..." } ], "warnings": [] } ] } } ``` `row` és el número de línia del teu fitxer — la capçalera és la línia 1, així que la primera fila de dades és la `2`. Corregeix al fitxer d'origen cada fila amb `status: "error"` i repeteix l'execució en sec fins que totes surtin `valid`. **L'execució en sec previsualitza només les 50 primeres files.** `total_rows` compta el fitxer sencer, però `rows[]` es talla a 50 — una execució en sec neta sobre un fitxer de 400 files no vol dir que de la 51 endavant estigui neta. Si l'export és gran, parteix-lo i fes l'execució en sec de cada tros. ### Pas 2 — la importació real [#client-import-run] La mateixa crida amb `dry_run=false` (o sense el flag). S'apliquen dos límits: * **Menys de 200 files per petició.** Un fitxer amb 200 files o més es rebutja amb `422 client_import_too_large` — la importació és síncrona per poder retornar el resultat per fila a la mateixa resposta. Parteix l'export. * **Una quota mensual de files per pla**: 100 files a Emprendedor, 1.000 a Empresario, sense límit a Enterprise. Compta les files realment importades, sumant totes les importacions del mes natural. La importació és **best-effort per fila**: cada fila és la seva pròpia transacció, així que una fila que falla no reverteix les que ja s'han creat. La resposta et diu exactament quines reenviar: ```json { "data": { "total": 128, "successful": 126, "failed": 2, "failures": [ { "index": 41, "error_code": "INVALID_FORMAT", "error_message": "...", "errors": [{ "param": "tax_id", "code": "INVALID_FORMAT", "message": "..." }] } ], "results": [ { "row": 43, "status": "error", "errors": [...], "warnings": [] } ] } } ``` `index` va des de 0 sobre les files de dades; `row` és la línia del fitxer (`index + 2`). Compte: `successful` compta les files **creades més les omeses**, perquè l'importador deduplica per `tax_id` —tant contra els clients que ja existeixen a la teva empresa com contra files repetides dins del mateix fitxer— i una fila omesa és un èxit, no una fallada. Això és el que fa segur repetir una importació, però també vol dir que `successful` no és el nombre de clients creats. A `results[]` només es detallen les files que van fallar. ### Pas 3 — reconciliar amb l'ID de Holded [#client-import-reconcile] **`external_id` no és un camp destí de la importació.** L'importador de fitxer escriu els camps de la taula de dalt i cap més, així que la columna `Id` de Holded no hi pot viatjar. Mapejar `"Id": "external_id"` **no** es rebutja: s'ignora en silenci, i la importació respon `200` com si hagués funcionat. Estampa l'id en una segona passada, com s'explica a sota. La correspondència que necessites —ID de Holded ↔ NIF— ja és al fitxer d'export que acabes de pujar. Estampa l'`external_id` després, amb una crida per client: 1. Resol el client pel NIF d'aquella fila: ```bash curl -X POST https://api.factuarea.com/v1/clients/find-by-tax-id \ -H "Authorization: Bearer fact_live_..." \ -H "Content-Type: application/json" \ -d '{ "tax_id": "B12345678" }' ``` Retorna `200` amb el client, o `404 client_not_found` si aquella fila va ser una de les que van fallar al pas 2. 2. Escriu-hi l'ID de Holded. El `PUT` d'un client és una actualització parcial, així que enviar només `external_id` deixa intacta la resta de camps: ```bash curl -X PUT https://api.factuarea.com/v1/clients/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01 \ -H "Authorization: Bearer fact_live_..." \ -H "Content-Type: application/json" \ -d '{ "external_id": "5e1c2a3b4f5d6e7f8a9b0c1d" }' ``` A partir d'aquí els clients importats es comporten igual que els que crea l'script: `POST /v1/clients/find-by-external-id` els resol pel seu ID de Holded, i la migració de factures els pot referenciar sense taula de correspondències. **Vols l'`external_id` d'una passada?** Doncs no facis servir l'importador de fitxer. `POST /v1/clients/bulk-create` admet fins a 500 payloads de client per lot, té el mateix flag `dry_run` i el mateix contracte de resultat per fila, i accepta `external_id` a cada payload — llegeix tu l'export i envia'l com a JSON. Mira [Operacions massives](/guides/bulk-operations). ## Checklist de migració [#checklist-de-migració] 1. **Inventari**: nombre de contactes, productes, factures històriques, webhooks actius. 2. **Mapeja amb `external_id`** (recomanat): escriu cada ID de Holded a l'`external_id` del recurs de Factuarea corresponent en crear-lo. Així no et cal una taula intermèdia `holded_id ↔ factuarea_id` — per resoldre una relació (factura → client) o per reexecutar la migració amb seguretat, cerca el registre amb `POST /v1/{recurs}/find-by-external-id` (body `{ "external_id": "" }`). Això és el que fa idempotent la migració. 3. **Migració per fases**: * Catàlegs: impostos, sèries, productes → primer. * Mestres: clients, proveïdors → segon. * Documents històrics: factures, pressupostos, etc. → tercer. 4. **Doble escriptura temporal**: durant 1–2 setmanes, escriu a totes dues plataformes. Reconcilia les diferències diàriament. 5. **Webhooks**: configura els nous endpoints, desplega el handler amb verificació HMAC i executa'l en paral·lel. 6. **Cut-over**: deixa d'escriure a Holded, deshabilita els webhooks allà. 7. **Suport**: contacta amb `support@factuarea.com` indicant el `request_id` davant de qualsevol incidència durant la migració. ## Diferències intencionades [#diferències-intencionades] Alguns comportaments de Holded **no repliquem** a propòsit: * **Anul·lar vs eliminar una factura**: Holded permet eliminar factures. Factuarea no — emetre i després eliminar és un anti-patró davant de l'AEAT. Fes servir `POST /v1/invoices/{id}/annul` (anul·lar) o emet una factura rectificativa. * **Editar una factura emesa**: Holded permet reemetre un PDF diferent. Factuarea bloqueja els canvis després de `sent` excepte `mark-paid`, `annul`, `create-corrective`. És deliberat. * **Calculadora d'IVA en línia**: Holded accepta el percentatge d'IVA a cada línia. Factuarea requereix una FK al catàleg d'impostos per garantir la coherència i els informes. Són decisions de producte, no limitacions tècniques. Si trobes un cas d'ús real que no puguem cobrir, contacta amb producte. --- # Tancament mensual del registre (/ca/guides/monthly-time-close) Un **tancament mensual** congela el registre de jornada d'un `(any, mes)` finalitzat. Pren un **snapshot** dels totals de saldo i del desglossament d'absències de cada empleat actiu —reutilitzant el contracte de balanços, sense recalcular— i **bloqueja el període** contra fitxatges retroactius i correccions. És el pas que converteix un ledger en curs en un registre mensual defensable. Tots els endpoints viuen sota `https://api.factuarea.com/v1`; tancar i reobrir usen `time_entries:write`, les lectures i exportacions usen `time_entries:read` (les exportacions per a nòmines usen `payroll_exports:read`). ## Tancar un mes [#close] `POST /v1/monthly-register-closes` tanca un mes finalitzat. `year` i `month` (1–12) són obligatoris. Un mes **que encara no ha acabat** retorna `422`; un període **ja tancat** retorna `409`. El tancament es crea en estat `closed` i la resposta porta una capçalera `Location` que hi apunta. ```bash curl -X POST https://api.factuarea.com/v1/monthly-register-closes \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "year": 2026, "month": 1 }' ``` Un tancament es mou entre dos estats, `closed ⇄ reopened`; cap no és terminal. **Reobrir** (`POST /v1/monthly-register-closes/{close}/reopen`) és una recuperació auditada d'un tancament erroni que torna a permetre escriptures al període. Re-tancar un mes reobert conserva el seu `id` original — el tancament reobert i re-tancat és el **mateix** recurs. Llista tancaments amb `GET /v1/monthly-register-closes` (ordenats per període descendent, filtrables per `year`) i obtén-ne un amb `GET /v1/monthly-register-closes/{close}`. ## Segellar-lo amb una signatura digital [#seal] `POST /v1/monthly-register-closes/{close}/seal` **segella** un registre `closed`: congela un digest SHA-256 canònic del snapshot i una **signatura RSA-SHA256 separada** feta amb el certificat de l'empresa. El registre queda a prova de manipulació i verificable de manera independent per un tercer. Hi ha **un segell per tancament** — tornar a segellar retorna `409`. Segellar un tancament que no està `closed` retorna `422`, i una empresa sense certificat actiu i usable retorna `422`. Recupera el segell i el seu **estat de verificació en viu** amb `GET /v1/monthly-register-closes/{close}/seal`: `verified` és `true` quan el snapshot i la signatura estan intactes; en cas contrari, `verification_reason` explica el desajust (`snapshot_mismatch`, `signature_invalid` o `certificate_unreadable`). ```bash curl -X POST https://api.factuarea.com/v1/monthly-register-closes/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/seal \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` El segell és opcional però recomanable: el tancament per si sol bloqueja el període, i el segell afegeix una signatura criptogràfica que permet a un auditor provar que el snapshot no ha canviat des que es va signar. ## Informe i exportacions [#exports] Tres sortides de lectura es construeixen des del **snapshot congelat**, de manera que els totals mai es desvien del full del moment del tancament. | Sortida | Endpoint | Què obtens | | ----------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Informe mensual | `GET /v1/monthly-register-closes/{close}/report` | Totals agregats de l'empresa més una fila per empleat (totals, desglossament d'absències, saldos) i el detall diari. Totals en minuts. | | Exportació del registre diari | `GET /v1/monthly-register-closes/{close}/export` | El registre diari com a full de càlcul en el format `rdley_8_2019`, llegit del ledger bloquejat. Descàrrega binària. | | Incidències per a nòmina | `GET /v1/monthly-register-closes/{close}/payroll-export` | Una fila per empleat (identitat fiscal, minuts treballats vs esperats, hores extra, saldo, absències aprovades per tipus) en `a3`, `sage` o `nominasol`. Descàrrega binària. | L'informe és un **recurs computat**: exposa `close_id`, mai un `id` propi. Per als fitxers d'exportació i de nòmina, `format` és opcional (per defecte `rdley_8_2019` i `a3` respectivament); un valor fora del catàleg retorna `422`, i un període sense tancament retorna `404`. Llista el programari de nòmina suportat amb `GET /v1/payroll-export-formats`. ```bash curl -G https://api.factuarea.com/v1/monthly-register-closes/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/payroll-export \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "format=a3" \ --output payroll-2026-01.xlsx ``` Consulta els esquemes a la [Referència d'API](/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.create). ## Flux típic [#flow] 1. **Tanca** el mes finalitzat (`POST .../monthly-register-closes`). 2. **Segella'l** si necessites un registre signat i verificable (`POST .../{close}/seal`). 3. **Informa o exporta** per a auditoria (`/report`), el fitxer d'inspecció (`/export`) o nòmina (`/payroll-export`). 4. Si detectes un error, **reobre**, corregeix les entrades i **re-tanca** — l'`id` es manté igual. ## Pròxims passos [#next] * [Fitxatges](/guides/time-clock) — les entrades i correccions que el tancament fotografia. * [Absències](/guides/absences) — les absències aprovades que apareixen a l'informe. --- # Paginació (/ca/guides/pagination) Tots els endpoints de llistat de l'API pública usen **paginació per cursor**. Mateixa semàntica que Stripe / Linear: pagines per un identificador opac (l'`id` del recurs), no per número de pàgina. Això garanteix resultats estables fins i tot quan es creen nous recursos durant la iteració. ## Paràmetres [#paràmetres] | Paràmetre | Tipus | Per defecte | Rang | Descripció | | ---------------- | ------- | ----------- | --------------- | ---------------------------------------------------------------------------------- | | `limit` | integer | `25` | `1`–`100` | Nombre d'elements per pàgina. | | `starting_after` | string | `null` | id (UUID v7) | Retorna els elements creats **després** del recurs el `id` del qual es passa. | | `ending_before` | string | `null` | id (UUID v7) | Retorna els elements creats **abans** del recurs el `id` del qual es passa. | | `sort` | string | `-created` | camp per recurs | Camp d'ordre; prefix `-` per a descendent (p.ex. `-total`). Vegeu [Ordre](#ordre). | `starting_after` i `ending_before` són mútuament excloents. Enviar tots dos en la mateixa petició respon `422` amb un embolcall d'error `invalid_request_error`. ## Forma de la resposta [#forma-de-la-resposta] ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "...": "..." }, { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "...": "..." } ], "has_more": true, "next_cursor": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c" } ``` * `data`: array de fins a `limit` elements, ordenats per `id` DESC (equivalent a `created_at` DESC perquè usem UUID v7). * `has_more`: `true` si hi ha més elements abans del primer de `data` (més nous) en paginar amb `starting_after`, o després de l'últim en paginar amb `ending_before`. * `next_cursor`: `id` de l'últim element de `data`. Passa'l com a `starting_after` en la petició següent per avançar. Quan no hi ha més elements, `has_more` és `false` i `next_cursor` és `null`. ## Iterar tots els resultats [#iterar-tots-els-resultats] ```python import os, requests def iterate(endpoint): cursor = None while True: params = {'limit': 100} if cursor: params['starting_after'] = cursor resp = requests.get( f'https://api.factuarea.com/v1/{endpoint}', params=params, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() body = resp.json() yield from body['data'] if not body['has_more']: break cursor = body['next_cursor'] for invoice in iterate('invoices'): print(invoice['id'], invoice['number']) ``` ```javascript async function* iterate(endpoint) { let cursor = null; while (true) { const url = new URL(`https://api.factuarea.com/v1/${endpoint}`); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('starting_after', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}` }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const body = await res.json(); for (const item of body.data) yield item; if (!body.has_more) break; cursor = body.next_cursor; } } for await (const invoice of iterate('invoices')) { console.log(invoice.id, invoice.number); } ``` ```bash cursor="" while : ; do if [ -z "$cursor" ]; then url="https://api.factuarea.com/v1/invoices?limit=100" else url="https://api.factuarea.com/v1/invoices?limit=100&starting_after=$cursor" fi resp=$(curl -s -H "Authorization: Bearer $FACTUAREA_API_KEY" "$url") echo "$resp" | jq -c '.data[]' has_more=$(echo "$resp" | jq -r '.has_more') cursor=$(echo "$resp" | jq -r '.next_cursor') [ "$has_more" = "true" ] || break done ``` ## Iterar amb l'SDK oficial [#iterar-amb-lsdk-oficial] Els [SDK de TypeScript i PHP](/sdks) amaguen el cursor completament: els mètodes de llistat retornen un iterador que recorre totes les pàgines per tu. ```ts const page = await factuarea.invoices.list({ status: "paid", limit: 50 }); // iterate every item across every page — cursors handled internally for await (const invoice of page) { console.log(invoice.id, invoice.number); } // or walk page by page page.data; // items on this page page.hasMore; // boolean page.nextCursor; // opaque cursor or null const next = await page.getNextPage(); // Page | null const all = await page.toArray(); // collect everything ``` ```php use Factuarea\Sdk\Custom\Pagination\PageIterator; use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest; $pages = new PageIterator( fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList( new PublicApiV1InvoicesListRequest(startingAfter: $cursor), )->rawResponse, ); foreach ($pages->items() as $invoice) { echo $invoice['id'], PHP_EOL; } ``` Consulta [SDKs › Paginar amb l'SDK](/sdks#paginating-with-the-sdk) per veure la superfície completa. ## Ordre [#ordre] Passa `?sort=` per ordenar un llistat. Un camp tot sol ordena de manera **ascendent**; el prefix `-` ordena de manera **descendent** (p.ex. `?sort=-total`). Si s'omet, el valor per defecte és **`-created`** — equivalent a `id` DESC, un ordre total i estable perquè usem UUID v7 (que codifica una marca de temps en els 48 bits alts, de manera que "els creats més recentment primer" no necessita cap columna `created_at` addicional). El cursor (`starting_after` / `ending_before`) continua funcionant amb l'ordre que triïs: el camp escollit és el criteri principal i el `id` del recurs és un criteri secundari estable, de manera que la paginació es manté determinista fins i tot quan diverses files comparteixen el mateix valor (a l'estil de Stripe). Els camps `sort` permesos estan acotats **per recurs** — enviar un camp no suportat respon `422` amb un embolcall d'error `invalid_request_error`: | Recurs | Camps `sort` permesos | | -------------------- | ------------------------------------------- | | `invoices` | `created`, `total`, `number` | | `quotes` | `created`, `total`, `number`, `valid_until` | | `proformas` | `created`, `total`, `number`, `valid_until` | | `delivery_notes` | `created`, `number`, `delivery_date` | | `purchase_invoices` | `created`, `total`, `issued_on`, `due_on` | | `recurring_invoices` | `created`, `next_run_at` | ```bash # Factures, total més alt primer curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "sort=-total" # Pressupostos per data de validesa, els que vencen abans primer curl -G https://api.factuarea.com/v1/quotes \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "sort=valid_until" ``` ## Per què no `?page=`? [#per-què-no-page] Les pàgines numèriques tenen problemes quan el conjunt de dades canvia durant la iteració: * Crear un recurs entre pàgines → duplica files. * Eliminar un recurs entre pàgines → omet files. * `COUNT(*)` és costós passades unes quantes milers de files. La paginació per cursor amb UUID v7 elimina tots dos: el cursor apunta a una posició estable en el temps, no a un desplaçament variable. Per això no hi ha cap paràmetre de desplaçament `?page=N` — l'única manera de paginar per una llista és el cursor `starting_after` / `ending_before`. Un valor de cursor invàlid respon `422` amb un embolcall d'error `invalid_request_error` (`code: parameter_invalid_cursor`): ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid_cursor", "message": "The provided cursor is not a valid resource id.", "param": "starting_after", "request_id": "req_..." } } ``` ## Cas d'ús: obtenir només resultats nous [#cas-dús-obtenir-només-resultats-nous] Si la teva integració fa polling cada N minuts, desa el `next_cursor` (l'`id` més recent que has vist) entre cada sondeig. A la passada següent usa `ending_before=` per obtenir només els elements **més nous** que aquell punt. ```python last_seen = load_last_cursor() # resource id stored in your DB resp = requests.get( 'https://api.factuarea.com/v1/invoices', params={'limit': 100, 'ending_before': last_seen} if last_seen else {'limit': 100}, headers={'Authorization': f"Bearer {API_KEY}"}, ) new_invoices = resp.json()['data'] if new_invoices: save_last_cursor(new_invoices[0]['id']) # the newest one ``` --- # Registrar pagaments (/ca/guides/payments) Les factures i les factures de compra mantenen un **ledger de pagaments**: una llista de pagaments individuals, cadascun amb el seu propi import, data i mètode. Registra els pagaments d'un en un a mesura que entra els diners — l'API recalcula els imports **cobrat** i **pendent** després de cada entrada i passa el document a `paid` quan el saldo arriba a zero. No existeix un estat «parcialment pagada» a part. L'avanç del cobrament es llegeix a partir de dos camps derivats, només de presentació, a la factura: `paid_amount` (suma del ledger) i `pending_amount` (`total − paid_amount`). Un document amb `pending_amount > 0` continua `pending`; aquell el `pending_amount` del qual arriba a `0` passa a `paid`. ## Registrar un pagament de venda [#registrar-un-pagament-de-venda] `POST /v1/invoices/{id}/payments` afegeix un pagament a una factura de venda. El body és petit: | Camp | Tipus | Requerit | Notes | | ---------------- | --------------------- | -------- | -------------------------------------------------------------- | | `amount` | number | **Sí** | Més gran que `0`. No pot superar `pending_amount`. | | `paid_on` | string (`YYYY-MM-DD`) | **Sí** | La data en què es va rebre els diners. | | `payment_method` | string (enum) | **Sí** | Un dels valors del catàleg (vegeu a sota). | | `reference` | string | No | La teva pròpia referència (p. ex. un número de transferència). | | `notes` | string | No | Nota interna lliure. | `payment_method` és un enum tancat de set valors: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`. Obtén el catàleg amb etiquetes des de [`GET /v1/payment-methods`](#payment-methods) en lloc de fixar els valors a mà. La resposta és `201 Created` amb el pagament acabat de crear sota `data`: ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } } ``` ```python import os, requests resp = requests.post( 'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments', json={ 'amount': 500.00, 'paid_on': '2026-05-20', 'payment_method': 'bank_transfer', 'reference': 'TRF-2026-0042', }, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() payment = resp.json()['data'] print(payment['id'], payment['amount']) ``` ```javascript const res = await fetch( 'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 500.0, paid_on: '2026-05-20', payment_method: 'bank_transfer', reference: 'TRF-2026-0042', }), }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(data.id, data.amount); ``` ```bash curl -s -X POST \ https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 500.00, "paid_on": "2026-05-20", "payment_method": "bank_transfer", "reference": "TRF-2026-0042" }' | jq '.data' ``` ## Pagaments parcials i saldo [#pagaments-parcials-i-saldo] El saldo en curs **no** viu a l'objecte del pagament — viu a la **factura**. Després de registrar un o diversos pagaments, llegeix la factura (`GET /v1/invoices/{id}`) per veure com està: ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "object": "invoice", "status": "pending", "total": 1210.00, "paid_amount": 500.00, "pending_amount": 710.00, "payments": { "detail": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } ], "total": 500.00, "pending": 710.00 } } } ``` * `paid_amount` / `pending_amount` — els totals cobrat i pendent. Sempre presents, calculats a partir del ledger. * `payments.total` / `payments.pending` — les mateixes dues xifres, reflectides dins de l'objecte `payments`. Sempre presents. * `payments.detail` — l'array de pagaments individuals. Es materialitza només a l'endpoint de **detall** (`GET /v1/invoices/{id}`); als endpoints de **llistat** arriba com a `[]` (mentre `total` i `pending` continuen poblats) perquè els llistats siguin lleugers. Fes servir el [sub-recurs](#list-payments) per obtenir el detall per separat. Quan l'últim pagament tanca el saldo (`pending_amount` arriba a `0`), la factura passa a `paid`. Un pagament l'`amount` del qual supera `pending_amount` es rebutja amb `422` i `subcode: "payment_exceeds_pending_amount"` (`param: "amount"`). Un pagament **exactament igual** a l'import pendent és vàlid i salda la factura. Consulta [Errors](/ca/guides/errors#business_rule_violation). ### Llistar pagaments [#list-payments] `GET /v1/invoices/{id}/payments` retorna el ledger complet d'una factura, del més recent al més antic. Una factura sense pagaments retorna `{ "data": [] }`, mai un `404`. ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } ] } ``` ## Pagaments de factura de compra [#pagaments-de-factura-de-compra] Les factures de compra mantenen el seu propi ledger (`total_retention`, la retenció IRPF agregada, viu al recurs de la factura de compra). El contracte és **asimètric** respecte al de venda — llegeix-lo amb atenció abans de reutilitzar codi: * `POST /v1/purchase_invoices/{id}/payments` retorna `201` amb el **pagament creat** sota `data` (objecte `purchase_invoice_payment`), no la factura completa. * `GET /v1/purchase_invoices/{id}/payments` retorna `{ "data": [...] }`, del més recent al més antic. * El body afegeix un `bank_account_id` opcional (enter), i aquí `payment_method` és un **string lliure** (màx. 30 caràcters), no l'enum tancat que es fa servir al costat de venda. | Camp | Tipus | Requerit | Notes | | ----------------- | --------------------- | -------- | -------------------------------------------------- | | `amount` | number | **Sí** | Més gran que `0`. No pot superar l'import pendent. | | `paid_on` | string (`YYYY-MM-DD`) | **Sí** | Entre la data d'emissió i avui. | | `payment_method` | string | **Sí** | Text lliure, màx. 30 caràcters. | | `bank_account_id` | integer | No | Compte bancari des del qual es va fer el pagament. | | `reference` | string | No | La teva pròpia referència. | | `notes` | string | No | Nota interna lliure. | ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "object": "purchase_invoice_payment", "amount": 423.50, "paid_on": "2026-05-21", "payment_method": "transferencia", "bank_account_id": 12, "reference": "TRF-2026-0099", "notes": null, "created_at": "2026-05-21T09:00:00Z" } } ``` ```python import os, requests resp = requests.post( 'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments', json={ 'amount': 423.50, 'paid_on': '2026-05-21', 'payment_method': 'transferencia', 'bank_account_id': 12, }, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() print(resp.json()['data']['id']) ``` ```javascript const res = await fetch( 'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 423.5, paid_on: '2026-05-21', payment_method: 'transferencia', bank_account_id: 12, }), }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(data.id); ``` ```bash curl -s -X POST \ https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 423.50, "paid_on": "2026-05-21", "payment_method": "transferencia", "bank_account_id": 12 }' | jq '.data' ``` Les regles de pagament de la factura de compra (`BR-PUR-019`) s'apliquen com a `422`: un import per sobre del saldo pendent (`subcode: "payment_exceeds_pending_amount"`), una data fora de `data_emissió … avui` (`subcode: "invalid_payment_date"`), o un pagament sobre una factura cancel·lada (`subcode: "purchase_invoice_not_payable"`). ## Mètodes de pagament [#payment-methods] `GET /v1/payment-methods` retorna el catàleg tancat que dona suport al camp `payment_method` de venda, cadascun amb un `value` i una etiqueta llegible (en castellà). És un catàleg d'enum global — no específic d'empresa. ```json { "data": [ { "value": "bank_transfer", "label": "Transferencia bancaria" }, { "value": "direct_debit", "label": "Domiciliación bancaria" }, { "value": "cash", "label": "Efectivo" }, { "value": "credit_card", "label": "Tarjeta de crédito" }, { "value": "check", "label": "Cheque" }, { "value": "paypal", "label": "PayPal" }, { "value": "other", "label": "Otro" } ] } ``` Llegeix-lo un cop en arrencar i mostra les etiquetes a la teva interfície; retorna el `value` a `payment_method`. ## Errors [#errors] * **`422` `payment_exceeds_pending_amount`** — l'import és més gran que el saldo pendent (`param: "amount"`). És una violació de regla de negoci, així que és `422`, mai `409`. * **`409`** en un `POST` de pagament es reserva per a l'embolcall estàndard d'[idempotència](/ca/guides/idempotency) / conflicte (un `Idempotency-Key` reutilitzat amb un body diferent, o un conflicte de concurrència) — no per a les dades del pagament en si. Consulta [Errors](/ca/guides/errors) per a l'embolcall complet i el catàleg de codis. --- # Presència (/ca/guides/presence) La **presència** respon a dues preguntes en viu: **qui treballa ara mateix?** i **qui és avui a l'oficina i qui en remot?** No és un CRUD sobre una taula pròpia — és un **read-model derivat** compost a partir de tres fonts: la **plantilla** d'empleats, l'**estat de fitxatge** derivat del ledger de jornada, i l'**horari** vigent. L'estat de treball en viu (`working`, `paused`, `finished`, `away`) i l'indicador d'arribada tard es **computen en llegir**, mai es persisteixen. A l'API v1, la presència és de **només lectura** (`presence:read`), sota `https://api.factuarea.com/v1`. No existeix l'scope `presence:write`: declarar la presencialitat oficina/remot és una tasca només-portal que fa el mateix empleat. ## El panell d'equip en viu [#live] `GET /v1/presence` retorna el panell en viu: un item per empleat actiu amb el seu estat de treball actual, des de quan té oberta la franja actual, i si va arribar tard respecte a la seva hora d'entrada [planificada](/guides/work-schedules), més comptadors agregats (treballant, en pausa, absent, en remot). ```bash curl https://api.factuarea.com/v1/presence \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` L'estat de treball es deriva de l'últim esdeveniment de la franja oberta de cada empleat al ledger: `clock_in`/`pause_end` → `working`, `pause_start` → `paused`, `clock_out` → `finished`, sense franja oberta → `away`. L'arribada tard compara el primer fitxatge d'entrada del dia amb l'hora planificada llegida de l'horari de l'empleat. ## Presencialitat diària oficina/remot [#daily] `GET /v1/presence/daily` llista la **presencialitat diària** —oficina enfront de remot— amb filtres per empleat, data o rang i paginació per cursor. `GET /v1/presence/{employee}` retorna la presència d'un sol empleat pel seu `id` (UUID v7); un empleat d'una altra empresa retorna `404`. ```bash curl -G https://api.factuarea.com/v1/presence/daily \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "date=2026-02-03" ``` La **presencialitat diària** (oficina o remot) és l'únic dada que la presència desa de veritat: un registre per empleat i dia. Declarar-la de nou per al mateix dia canvia la localització en comptes de crear un duplicat. Consulta els esquemes a la [Referència d'API](/api-reference/presence/public-api.v1.presence.live). La presència és de només lectura a l'API. Els empleats declaren si són a l'oficina o en remot des del portal — no hi ha endpoint públic d'escriptura, així que una integració llegeix la presència, no la fixa. ## Flux típic [#flow] 1. Sondeja `GET /v1/presence` per a un tauler en viu de qui treballa, està en pausa o absent. 2. Llegeix `GET /v1/presence/daily` per veure el repartiment oficina/remot d'una data. 3. Aprofundeix en una persona amb `GET /v1/presence/{employee}`. Com que la presència és derivada, les xifres sempre reflecteixen l'estat actual del ledger i dels horaris — mai necessites mantenir una taula de presència a part sincronitzada. ## Pròxims passos [#next] * [Fitxatges](/guides/time-clock) — el ledger del qual es deriva l'estat en viu. * [Horaris](/guides/work-schedules) — l'hora planificada que usa l'indicador d'arribada tard. --- # Inici ràpid (/ca/guides/quickstart) Aquesta guia et porta d'una API key acabada de crear a una factura real (en sandbox) en cinc passos. Cada crida de sota és **executable copiant i enganxant** contra una key `fact_test_` — sense emails reals, sense enviament a l'AEAT, sense consumir numeració de producció. Consulta [Mode de prova & sandbox](/guides/test-mode) per veure què desactiva el mode "test". **Prefereixes un SDK?** Si treballes amb TypeScript/Node o PHP, els [SDKs oficials](/sdks) embolcallen tot aquest flux amb reintents integrats, idempotència, paginació per cursor i errors tipats — `npm install @factuarea/sdk` o `composer require factuarea/factuarea-php`. Els passos HTTP en brut de sota funcionen en qualsevol llenguatge i mostren exactament el que l'SDK envia per dins. Executa-ho tot primer amb una key **`fact_test_`**. La superfície de l'API és idèntica en producció i en test — quan el teu flux funcioni de principi a fi, canvia el prefix a `fact_live_` per passar a producció. Aconsegueix una key de test a [Settings → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys). Exporta la teva key un cop perquè cada snippet la reculli: ```bash export FACTUAREA_API_KEY="fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" ``` La base URL és `https://api.factuarea.com/v1`. Autentica't amb `Authorization: Bearer` (o amb el header equivalent `X-API-Key`). Els identificadors són valors `id` opacs (UUID v7); els copies d'una resposta a la següent. **Verifica la teva key** `GET /v1/account` introspecciona la credencial: l'empresa a la qual pertany, el pla, l'estat de l'accés a l'API i els **scopes** i el **tier** de rate limit de la pròpia key (derivat del pla). Necessita el scope `account:read`. ```bash curl https://api.factuarea.com/v1/account \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ```json { "data": { "object": "account", "company": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Acme Soluciones SL", "tax_id": "B12345678" }, "plan": { "slug": "empresario", "name": "Empresario" }, "addon": { "active": true, "in_grace": false, "expires_at": null }, "api_key": { "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "name": "Sandbox integration", "prefix": "fact_test_3pXnR2Vb", "scopes": [ "account:read", "series:read", "taxes:read", "clients:write", "invoices:write", "invoices:send", "pdfs:read" ], "tier": "pro", "created_at": "2026-05-01T09:30:00Z", "last_used_at": "2026-06-02T08:12:00Z", "expires_at": null } } } ``` Un `200` aquí significa que la key és vàlida i pots veure exactament quins scopes porta. Si reps `401 invalid_api_key`, revisa el valor; si un pas posterior falla amb `403 insufficient_scope`, l'array `scopes` de dalt et diu què falta. **Aconsegueix els ids que necessitaràs** Una factura referencia una **sèrie** (la seva numeració) i cada línia referencia un **tipus impositiu**. Tots dos són recursos existents que llistes un cop i reutilitzes. **Un id de sèrie** `GET /v1/series` retorna les teves sèries de numeració. Tria'n una el `document_type` de la qual sigui `invoice` (la marcada com a `is_default` és una opció segura). Necessita `series:read`. ```bash curl "https://api.factuarea.com/v1/series?document_type=invoice" \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "object": "series", "code": "F-2026", "name": "Facturas 2026", "document_type": "invoice", "prefix": "F-2026-", "next_number": 46, "year_reset": true, "is_default": true, "is_active": true, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-20T11:30:00Z" } ], "has_more": false, "next_cursor": null } ``` Copia l'`id` (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e`) — aquest és el teu `series_id`. **Un id de tipus impositiu** `GET /v1/taxes` retorna el catàleg d'impostos (impostos globals del sistema + els teus personalitzats). Per a una línia de factura espanyola estàndard vols el tipus d'IVA al 21% — busca `type: "vat"` i `rate: 21`. Necessita `taxes:read`. ```bash curl "https://api.factuarea.com/v1/taxes?type=vat" \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", "object": "tax", "name": "IVA general 21%", "code": "IVA21", "rate": 21, "type": "vat", "applies_to": "both", "country": "ES", "is_default": true, "is_active": true, "is_system": true } ], "has_more": false, "next_cursor": null } ``` Copia aquest `id` — en una línia de factura va a `tax_rate_id`. **`tax_rate_id` vs `tax_rate`.** En una línia pots referenciar un tipus del catàleg per `tax_rate_id`, o saltar-te la cerca i passar el percentatge numèric directament com a `tax_rate` (p. ex. `"tax_rate": 21`). Fes servir l'un o l'altre per línia — `tax_rate_id` manté la línia vinculada al teu catàleg, `tax_rate` és un override inline ràpid. **Crea un client** La factura necessita algú a qui facturar. El body mínim de client és `name` més `tax_id` (l'identificador fiscal espanyol — NIF/CIF/NIE). Necessita `clients:write`. Això és una escriptura — envia una `Idempotency-Key` perquè una petició reintentada no creï mai un client duplicat. Consulta [Idempotència](/guides/idempotency). ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Cliente Demo SL", "tax_id": "B98765432" }' ``` ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "object": "client", "name": "Cliente Demo SL", "commercial_name": null, "tax_id": "B98765432", "vat_id": null, "email": null, "phone": null, "contact_person": null, "billing_emails": [], "address": { "line1": null, "postal_code": null, "city": null, "province": null, "country": null }, "coordinates": null, "notes": null, "metadata": {}, "is_active": true, "created_at": "2026-06-02T10:30:00Z", "updated_at": "2026-06-02T10:30:00Z" } } ``` (Els camps opcionals que no has enviat tornen com a `null`; `address` sempre és un objecte les subclaus del qual s'omplen a mesura que les proporciones.) Copia l'`id` retornat (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01`) — aquest és el teu `client_id`. **Crea la factura** Ara combina els tres ids. `POST /v1/invoices` requereix `client_id`, `series_id`, `issued_on`, `due_on` i com a mínim una línia. Cada línia necessita `description`, `quantity` i `unit_price`; afegeix `tax_rate_id` (o `tax_rate`) per aplicar IVA. Opcionals per línia: `discount_percent` i `product_id`. Necessita `invoices:write`. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "issued_on": "2026-06-02", "due_on": "2026-07-02", "lines": [ { "description": "Consultoría — junio 2026", "quantity": 10, "unit_price": 100, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", "discount_percent": 0 } ] }' ``` L'API calcula per tu els totals de la línia i del document i retorna l'embolcall de la factura. Una factura acabada de crear comença com a **esborrany**: encara sense `number` definitiu (`is_number_assigned: false`). ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42", "object": "invoice", "number": null, "is_number_assigned": false, "type": "F1", "series": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "code": "F-2026" }, "client": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Cliente Demo SL" }, "status": "draft", "issued_on": "2026-06-02", "due_on": "2026-07-02", "subtotal": 1000, "taxes_total": 210, "total": 1210, "currency": "EUR", "notes": null, "lines": [ { "object": "invoice_line", "description": "Consultoría — junio 2026", "product": null, "quantity": 10, "unit_price": 100, "tax_rate": 21, "discount_percent": 0, "subtotal": 1000, "taxes": 210, "total": 1210 } ], "metadata": {}, "operation_regime": "general", "verifactu_status": "not_applicable", "is_corrective": false, "corrective": null, "payment": null, "public_link": null, "substituted_by": null, "recurring": null, "paid_at": null, "paid_on": null, "sent_at": null, "voided_at": null, "void_reason": null, "created_at": "2026-06-02T10:31:00Z", "updated_at": "2026-06-02T10:31:00Z" } } ``` Fixa't en els camps monetaris calculats: el `subtotal` de la línia (`10 × 100 = 1000`), els seus `taxes` (`21%` de `1000 = 210`) i el `total` (`1210`), agregats en el `subtotal` / `taxes_total` / `total` de la factura. Copia l'`id` de la factura (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42`) per al pas següent. **Obtén el PDF i envia'l** Amb l'`id` de la factura pots descarregar el seu PDF i enviar-lo per email al client. `GET /v1/invoices/{id}/pdf` transmet el PDF binari (`application/pdf`). Necessita `pdfs:read`. Desa'l directament a un fitxer amb l'opció `-o` de curl: ```bash curl https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/pdf \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -o invoice.pdf ``` `POST /v1/invoices/{id}/send` envia la factura per email al client. Sense body fa servir l'email del client registrat a la fitxa; pots sobreescriure el destinatari i la còpia amb `to`, `cc`, `bcc`, `subject` i `body`. Necessita `invoices:send`. Com que estàs amb una key `fact_test_`, l'email **no s'entrega** a cap destinatari real (els efectes del sandbox estan desactivats). La crida igualment té èxit i la factura transiciona com ho faria en producció — perfecte per muntar el teu flux sense fer spam a ningú. ```bash curl -X POST https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/send \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "to": "demo@example.com", "subject": "Tu factura de Acme Soluciones SL" }' ``` La resposta és l'embolcall actualitzat de la factura (amb la mateixa forma que dalt), ara amb `sent_at` poblat. ## I ja està [#i-ja-està] Has verificat una key, has descobert els ids que necessita, has creat un client, has emès una factura amb totals calculats al servidor i l'has enviat — tot contra un sandbox aïllat. A partir d'aquí, apunta el mateix codi a una key `fact_live_` per operar sobre la teva empresa real. Executa aquest mateix flux amb @factuarea/sdk (TypeScript) o factuarea/factuarea-php — reintents, idempotència, paginació i errors tipats inclosos. Què desactiva una key fact\_test\_, com s'aïlla l'empresa sandbox, i com promocionar la teva integració a producció. --- # Límits de peticions (/ca/guides/rate-limits) L'API pública aplica dos nivells de quota per garantir l'equitat entre tenants i protegir el backend davant de pics: 1. **Quota per minut** (finestra lliscant). 2. **Quota mensual** (calendari natural, es reinicia el dia 1 a les 00:00 Europe/Madrid). Els límits depenen del **tier** de la teva API key. El tier es **deriva del pla de Factuarea de la teva empresa** (o d'un [boost de capacitat](#capacity-boost) actiu quan és superior) — mai no es fixa per clau ni per petició, i s'actualitza automàticament quan el teu pla canvia. ## Nivells [#nivells] | Tier | Per minut | Per mes | Inclòs amb | | ----------- | ------------- | ------------- | -------------------------------------------- | | **Free** | 10 rpm | 100 requests | El trial de 10 dies. | | **Starter** | 30 rpm | 5,000 | El pla Emprendedor. | | **Pro** | 300 rpm | 50,000 | El pla Empresario. | | **Scale** | Personalitzat | Personalitzat | El pla Enterprise (o un boost de capacitat). | Els tiers són acumulatius: un cop esgotada la quota mensual reps `429 rate_limit_exceeded` fins al dia 1 del mes següent. La quota per minut es reinicia amb una finestra lliscant. ## Boost de capacitat [#capacity-boost] Si necessites més capacitat d'API sense canviar de pla, subscriu-te a un **boost de capacitat** des del dashboard: un tier **estrictament superior** al que atorga el teu pla (per exemple, Starter → Pro). Mentre el boost està actiu, totes les teves claus fan servir el tier del boost. Comprar un tier igual o inferior al del teu pla retorna `422 boost_not_applicable`. ## Finestra lliscant [#finestra-lliscant] El cub per minut **no** és una finestra fixa de "60 segons des de les 12:00". És una finestra lliscant: en qualsevol moment, l'API compta quantes peticions acceptades hi ha en els darrers 60 segons per a la teva clau. Quan el comptador iguala el límit, les peticions següents responen `429` fins que passa prou temps perquè les primeres peticions "surtin" de la finestra. **Per què**: no hi ha cap "minut de gràcia" cada 60 segons en què poguessis enviar el doble del límit. Més just i més estable sota trànsit real. ## Capçaleres de resposta [#capçaleres-de-resposta] Tota resposta (inclòs 429) inclou: | Capçalera | Significat | | ----------------------- | --------------------------------------------------------------------------- | | `X-RateLimit-Limit` | Límit per minut del teu tier. | | `X-RateLimit-Remaining` | Peticions restants a la finestra actual. | | `X-RateLimit-Reset` | Timestamp UNIX en què s'allibera un lloc (una petició surt de la finestra). | | `Retry-After` | Només en `429`. Segons fins que pots reintentar. | Exemple de capçaleres en una resposta `200`: ```http HTTP/1.1 200 OK X-RateLimit-Limit: 300 X-RateLimit-Remaining: 287 X-RateLimit-Reset: 1747314060 ``` I en un `429`: ```http HTTP/1.1 429 Too Many Requests Retry-After: 7 X-RateLimit-Limit: 30 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1747314007 ``` ## Codi d'error [#codi-derror] ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Has superado el límite de peticiones. Vuelve a intentarlo en unos segundos.", "request_id": "req_..." } } ``` Superar la quota per minut o la mensual respon `429` amb `type: rate_limit_error` i `code: rate_limit_exceeded`. La capçalera `Retry-After` (i el missatge) t'indiquen quant has d'esperar. Els errors d'autenticació repetits es limiten per separat amb `code: too_many_auth_failures`. ## Bones pràctiques [#bones-pràctiques] ### 1. Respecta Retry-After [#1-respecta-retry-after] ```python import time, requests def call_with_retry(url, **kwargs): while True: resp = requests.get(url, **kwargs) if resp.status_code != 429: return resp sleep = int(resp.headers.get('Retry-After', 1)) time.sleep(sleep) ``` ### 2. Back-off exponencial amb jitter [#2-back-off-exponencial-amb-jitter] Per a `5xx`, on no hi ha `Retry-After`: ```python import random, time def backoff(attempt): return min(60, (2 ** attempt) * 0.1 + random.uniform(0, 0.5)) for attempt in range(5): resp = requests.get(url) if resp.status_code < 500: break time.sleep(backoff(attempt)) ``` ### 3. Monitoritza X-RateLimit-Remaining [#3-monitoritza-x-ratelimit-remaining] Si la teva integració s'acosta de manera consistent al 10% del límit, considera: * Pujar de tier. * Agrupar en lots: en comptes de N POSTs, agrega i fes 1 POST. * Cachejar lectures freqüents (productes, impostos, sèries). * Subscriure't a webhooks en comptes de fer polling. ### 4. Webhooks > polling [#4-webhooks--polling] Si fas polling de `/v1/invoices?status=paid` cada minut per detectar pagaments consumeixes 30 rpm només per a això. Subscriu-te a l'esdeveniment `invoice.paid` i redueix-ho a 0 peticions. ### 5. Claus per integració [#5-claus-per-integració] Si tens dues integracions (un dashboard intern + un cron d'exportació), crea **dues claus diferents**: cada clau té els seus propis cubs per minut i mensual, així un cron pesat no esgota el pressupost d'un dashboard interactiu. ## Quotes administratives [#quotes-administratives] Alguns endpoints tenen quotes addicionals **independents** del rate limit principal: | Endpoint | Quota | | ----------------------------- | ------------------------------------------------ | | `POST /v1/webhook_endpoints` | Nombre limitat d'endpoints per empresa. | | `POST /v1/invoices/{id}/send` | Limitat per factura per evitar emails duplicats. | | `GET /v1/invoices/{id}/pdf` | Generacions de PDF limitades per minut. | Aquests límits responen `429` amb `type: rate_limit_error` i un missatge específic. ## Pujada de tier [#pujada-de-tier] Canviar de tier no requereix rotar claus. Quan el teu pla canvia (o s'activa un boost de capacitat): 1. Les noves quotes apliquen immediatament. 2. La quota mensual consumida al tier anterior **no es reinicia**: només creix el topall mensual. 3. Les claus existents conserven el seu `id`; el nou tier s'aplica a totes automàticament. --- # Factures recurrents (/ca/guides/recurring-invoices) Una **factura recurrent** és una plantilla més una cadència: Factuarea genera una factura real a cada execució programada. Aquesta guia cobreix els controls que van més enllà del create/update bàsic — ometre un cicle, arrencar una recurrència a partir d'una factura existent, l'enviament automàtic per correu, la previsualització del document calculat i els camps fiscals per línia que exigeix VeriFactu. Tots els endpoints de sota viuen sota `https://api.factuarea.com/v1` i usen el mateix [embolcall d'error](/guides/errors), [paginació per cursor](/guides/pagination) i [scopes](/guides/scopes-and-irreversibility) que la resta de l'API. ## Ometre la pròxima generació [#ometre-la-pròxima-generació] ```http POST /v1/recurring_invoices/{recurring_invoice}/skip ``` Avança `next_run_at` exactament un període **sense generar cap factura** per al cicle actual. L'ocurrència omesa **no** compta per a `max_occurrences` — el comptador de factures generades no es mou. Fes-lo servir per saltar-te un període de facturació (festius, un client en pausa) mantenint intacte el calendari. Requereix l'scope `recurring_invoices:write`. Retorna `200` amb el recurs de la factura recurrent (fixa't en el `next_run_at` avançat). Una recurrència cancel·lada o completada respon `422`; una factura recurrent que pertany a una altra empresa respon `404`. ```bash curl -X POST https://api.factuarea.com/v1/recurring_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/skip \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Ometre registra una entrada `skipped` a l'activitat de la factura recurrent — l'historial mostra que el cicle es va ometre expressament, no que es va perdre. ## Crear una recurrència des d'una factura existent [#crear-una-recurrència-des-duna-factura-existent] ```http POST /v1/invoices/{invoice}/create-recurring ``` Copia les **línies, el client i la sèrie** d'una factura origen en una factura recurrent nova (amb el seu propi UUID v7) i aplica la cadència indicada al cos. La factura origen queda intacta. Requereix l'scope `recurring_invoices:write`. | Camp | Tipus | Obligatori | Notes | | ------------------ | --------------------- | ---------- | ------------------------------------------------------------------------------ | | `frequency` | string | sí | `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual`, `yearly`. | | `start_on` | string (`YYYY-MM-DD`) | sí | Primera execució programada. | | `end_on` | string (`YYYY-MM-DD`) | no | Última execució permesa. | | `name` | string | no | Etiqueta de la recurrència (≤255). | | `description` | string | no | | | `notes` | string | no | | | `metadata` | object | no | Els teus parells clau/valor. | | `holiday_handling` | string | no | Com desplaçar una execució que cau en festiu. | | `days_before_due` | integer | no | Desfasament de venciment de cada factura generada. | | `max_occurrences` | integer | no | Aturar després de N factures generades. | | `auto_delivery` | object | no | Vegeu [Enviament automàtic](#enviament-automàtic). | Retorna `201` amb la nova factura recurrent i una capçalera `Location` que hi apunta. Una factura origen que pertany a una altra empresa respon `404`. ```bash curl -X POST https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/create-recurring \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "frequency": "monthly", "start_on": "2026-07-01", "max_occurrences": 12, "auto_delivery": { "send_automatically": true, "recipients": ["billing@acme.example"] } }' ``` ## Enviament automàtic [#enviament-automàtic] L'objecte `auto_delivery` — disponible en crear i actualitzar una recurrent i a `create-recurring` — envia per correu cada factura generada de manera automàtica. | Camp | Tipus | Notes | | -------------------- | --------- | ----------------------------------------------------------- | | `send_automatically` | boolean | Interruptor mestre. `false` desactiva l'enviament. | | `recipients` | string\[] | Destinataris principals (email). | | `cc` | string\[] | Destinataris en còpia (email). | | `subject` | string | Assumpte personalitzat (≤255). `null` usa el predeterminat. | | `body` | string | Cos personalitzat (≤5000). `null` usa el predeterminat. | Quan `send_automatically` és `true`, cada factura generada s'envia per correu a `recipients` (amb `cc` opcional) usant `subject`/`body`. Posar `send_automatically` a `false` desactiva l'enviament. Una llista `recipients` buida amb `send_automatically: true` es rebutja amb `422` — no hi ha a qui enviar. ```json { "auto_delivery": { "send_automatically": true, "recipients": ["billing@acme.example"], "cc": ["copy@acme.example"], "subject": "La teva factura mensual", "body": "Hola, aquí tens la teva factura d'aquest període." } } ``` ## Previsualitzar el document calculat [#previsualitzar-el-document-calculat] ```http GET /v1/recurring_invoices/{recurring_invoice}/preview ``` Sense `expand`, `preview` retorna només la **previsió de dates** — les pròximes dates d'execució (usa `count` per controlar quantes). Passa `expand=document` per calcular **a més** el pròxim document en sec: la resposta afegeix un bloc `next_invoice` amb les `lines` resoltes i els `totals` (`subtotal`, `tax`, `total`), construïts des de `template_data` **sense persistir res**. Fes-lo servir per mostrar al client què contindrà exactament la pròxima factura abans d'emetre-la. ```bash curl -G https://api.factuarea.com/v1/recurring_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/preview \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "expand=document" ``` ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "recurring_invoice" }, "next_invoice": { "lines": [ { "description": "Quota mensual", "quantity": 1, "unit_price": 500, "subtotal": 500 } ], "totals": { "subtotal": 500, "tax": 105, "total": 605 } } } ``` `preview` mai crea cap factura. És una lectura pura — els totals en sec es calculen en memòria des de `template_data`. ## Camps fiscals per línia [#camps-fiscals-per-línia] Cada línia de `template_data` accepta els camps fiscals que necessiten VeriFactu i el model tributari espanyol. Es traslladen a cada factura generada. | Camp | Tipus | Notes | | ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `exemption_reason` | string | Causa d'exempció / no subjecció segons LIVA. Un de `E1`–`E6`, `N1`, `N2`. `null` si no s'informa. | | `regime_key` | string | Clau de règim VeriFactu (`ClaveRegimen`, llista L8.1 de l'AEAT). `null` si no s'informa. | | `retention` | number | Percentatge de retenció IRPF (0–100). | | `retention_rate_id` | string (UUID v7) | Impost del catàleg aplicat com a retenció IRPF. Opac — no canvia el càlcul de `taxes`/`total` (ho fa el percentatge pla `retention`). | | `surcharge` | number | Percentatge de recàrrec d'equivalència (0–100). | | `surcharge_rate_id` | string (UUID v7) | Impost del catàleg aplicat com a recàrrec d'equivalència. Opac — el percentatge pla `surcharge` governa el càlcul. | El recàrrec d'equivalència va lligat per llei a l'IVA de la línia. Els únics parells legals `tax_rate` → `surcharge` són: | IVA (`tax_rate`) | Recàrrec (`surcharge`) | | ---------------- | ---------------------- | | `21` | `5.2` | | `10` | `1.4` | | `4` | `0.5` | Enviar un `exemption_reason`, `regime_key`, `retention_rate_id` o `surcharge_rate_id` fora del seu catàleg respon `422` amb una llista `allowed_values` a l'error. Un parell IVA↔recàrrec il·legal (p.ex. `21` amb `1.4`) també es rebutja amb `422`. ```json { "template_data": { "lines": [ { "description": "Consultoria", "quantity": 1, "unit_price": 1000, "tax_rate": 21, "surcharge": 5.2, "retention": 15, "regime_key": "01", "exemption_reason": null } ] } } ``` --- # Claus de règim (/ca/guides/regime-keys) La facturació espanyola sobrecarrega la paraula *règim*. Tres conceptes diferents la comparteixen, viuen en nivells diferents del document i només un és una cosa que enviïs per l'API pública: | Concepte | Nivell | El fixes tu a la v1? | Determina | | ------------------------------------------------------------------------------------------ | ----------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------- | | **Règim d'operació** — interior, intracomunitari, exportació, inversió del subjecte passiu | Capçalera de la factura | **No.** Només lectura. | La qualificació AEAT de l'operació (`S1`, `S2`, `E5`, `E2`). | | **Clau de règim** (`ClaveRegimen`, llista AEAT L8.1) | Línia de factura | **Sí** — `lines[].regime_key` | El codi de règim especial declarat per a aquella línia. | | **Règim indirecte** — IVA, IGIC, IPSI | Línia de factura | Sí — `lines[].indirect_tax_regime` | Quin impost s'aplica, per començar. Vegeu [Impostos territorials](/guides/territorial-taxes). | Confondre els dos primers és, de bon tros, la causa més habitual d'una factura mal qualificada. Aquesta pàgina els separa. ## Quan aplica [#when] Sempre: tota factura emesa declara una qualificació i, per a gairebé tots els règims fiscals, una clau de règim. El que varia és si deixes totes dues a la derivació o les declares explícitament per línia. Declara una clau de règim explícita quan l'operació pertanyi a un règim especial — béns usats, agències de viatges, criteri de caixa, agricultura, recàrrec d'equivalència, vendes a distància per finestreta única. La derivació per capçalera només produeix el règim general o l'exportació, així que **la granularitat del catàleg complet només s'assoleix per línia**. ## El catàleg tancat [#catalog] `lines[].regime_key` accepta exactament aquests disset codis de dos dígits, de la llista `ClaveRegimen` L8.1 de l'AEAT ([`BR-INV-031`](#traceability)). Qualsevol altre valor respon `422` amb la llista completa a `allowed_values`. | Codi | Règim | | ---- | ---------------------------------------------------------------------------------------------------------------------------- | | `01` | Règim general. | | `02` | Exportació. | | `03` | Béns usats, objectes d'art, antiguitats i objectes de col·lecció (REBU). | | `04` | Or d'inversió. | | `05` | Agències de viatges. | | `06` | Grup d'entitats en IVA, nivell avançat. | | `07` | Règim especial del criteri de caixa. | | `08` | Operacions subjectes a l'IPSI o a l'IGIC. | | `09` | Facturació de prestacions de serveis d'agències de viatges que actuen com a mediadores en nom i per compte d'altri. | | `10` | Cobraments per compte de tercers d'honoraris professionals o de drets derivats de la propietat industrial, d'autor o altres. | | `11` | Operacions d'arrendament de local de negoci subjectes a retenció. | | `14` | Factura amb IVA pendent de meritació — certificacions d'obra amb una Administració pública com a destinatària. | | `15` | Factura amb IVA pendent de meritació — operacions de tracte successiu. | | `17` | Operacions acollides al Capítol XI del Títol IX — finestreta única (OSS i IOSS). | | `18` | Recàrrec d'equivalència. | | `19` | Agricultura, ramaderia i pesca (REAGYP). | | `20` | Règim simplificat. | Els números `12`, `13` i `16` hi falten a propòsit — no formen part de la llista, i enviar-los es rebutja igual que qualsevol altre valor fora del catàleg. ## Què envia l'API [#api] `regime_key` és un camp **opcional, per línia i additiu** de [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) i [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). Una línia que l'omet cau a la clau derivada de la capçalera de la factura: ```json { "lines": [ { "description": "Reventa de maquinaria de ocasión", "quantity": 1, "unit_price": 100, "regime_key": "03" }, { "description": "Servicio de instalación", "quantity": 1, "unit_price": 50, "tax_rate": 21 } ] } ``` La primera línia declara el règim de béns usats; la segona, sense clau, es deriva de la capçalera. Ometre el camp a totes les línies reprodueix exactament el comportament que existia abans d'introduir les claus per línia, ***huella* inclosa** — que és la raó per la qual el camp és additiu i no obligatori ([`BR-INV-031`](#traceability)). Tres dels quatre exemples publicats de creació de factura —`b2c`, `intracomunitario_bienes` i `con_irpf`, al desplegable d'exemples del cos de petició— declaren `regime_key: "01"` de manera explícita en lloc de recolzar-se en el valor derivat. El quart, `b2b_nacional`, l'omet i deixa que el règim el posi la capçalera, cosa igualment vàlida. Copia el costum explícit: una clau per línia es documenta a si mateixa i sobreviu a un canvi en la derivació per capçalera. ### El règim de capçalera és de només lectura a la v1 [#header-readonly] L'objecte factura retorna `operation_regime`, i ni l'operació de creació ni la d'actualització no l'accepten. **Tota factura creada per l'API pública neix sota el règim general.** La causa d'exempció a nivell de document —`exemption_reason` a l'objecte factura— és de només lectura pel mateix motiu. La conseqüència és concreta i convé dir-la sense embuts: la qualificació derivada de la capçalera serà `S1` en qualsevol factura creada per la v1, així que **l'exempció i la no subjecció s'han de declarar per línia**, amb `lines[].exemption_reason`. És exactament el que fa l'exemple `intracomunitario_bienes` —`tax_rate: 0` més `exemption_reason: "E5"`— en lloc de recolzar-se en un règim de capçalera que no pot fixar. Vegeu [Classificació fiscal i exempcions per línia](/guides/line-tax-classification-and-exemptions) per al catàleg de línia, i [Abast i limitacions](/guides/scope-and-limitations) per a què permet i què no aquesta frontera. ### El catàleg llegible per màquina [#tax-catalog] `GET /v1/tax-catalog` (scope `taxes:read`) publica els catàlegs fiscals que descriu aquesta pàgina — règims indirectes amb els seus tipus vàlids i els seus codis AEAT, règims d'operació amb les seves mencions legals, causes d'exempció amb el seu article de la LIVA, tipus de retenció del sistema i els parells legals d'IVA i recàrrec — amb etiquetes en espanyol, anglès i català a cada resposta. ```bash curl https://api.factuarea.com/v1/tax-catalog \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Dues propietats fan segur posar-lo a la memòria cau de manera agressiva ([`BR-TAX-028`](#traceability)): és **idèntic per a totes les empreses** —la consulta no porta identificador d'empresa i cap de les seves fonts no està acotada a un tenant, així que dues API keys reben cossos idèntics byte a byte i per tant el mateix `ETag`— i es deriva dels objectes de valor tancats del codi i no d'una llista copiada, de manera que un cas nou hi apareix automàticament en lloc de desincronitzar-se en silenci. Els tipus de retenció es publiquen en **positiu**, sigui quin sigui el signe amb què estiguin emmagatzemats. El bloc es filtra per «impost del sistema», no per «actiu»: un tipus que una empresa hagi desactivat continua formant part del catàleg legal, i un impost propi creat per un tenant no hi apareix mai. ## Què surt al PDF [#pdf] La clau de règim en si **no s'imprimeix**. El que veu qui llegeix el document és la menció legal derivada del règim d'operació — la referència a l'art. 25 LIVA en un lliurament intracomunitari, a l'art. 21 en una operació amb tercers països, a l'art. 84.Uno.2 en la inversió del subjecte passiu — i res en absolut per al règim general, que no necessita menció ([`BR-TAX-024`](#traceability)). Com que aquestes mencions deriven del règim de capçalera, i el règim de capçalera no es pot fixar a la v1, una factura creada per l'API pública no imprimeix cap menció automàtica de règim. Fes servir `notes` si el document necessita declarar l'exempció en prosa. ## Què arriba a l'AEAT [#aeat] Viatgen dos camps diferents per cada grup de desglossament, i responen preguntes diferents. **La qualificació** respon a «quina classe d'operació és aquesta?», i es deriva del règim de capçalera ([`BR-VFC-029`](#traceability)): | Règim d'operació de capçalera | Qualificació | Què rep l'AEAT | | ----------------------------- | ------------ | ------------------------------------------------------------------------------------ | | General | `S1` | Subjecta i no exempta, quota d'IVA `base × tipus`. | | Inversió del subjecte passiu | `S2` | Subjecta i **no** exempta, quota forçada a **0** — l'autorepercuteix el destinatari. | | Intracomunitari | `E5` | Subjecta i exempta, art. 25 LIVA. | | Importació o exportació | `E2` | Subjecta i exempta, art. 21 LIVA. | Els codis `E1`, `E3`, `E4` i `E6` existeixen al catàleg de l'AEAT però aquesta derivació no els produeix mai: només s'assoleixen com a causa d'exempció de **línia**. Una línia que en declari una guanya al valor derivat de la capçalera ([`BR-INV-032`](#traceability)). **La clau de règim** respon a «sota quin règim especial?», i *no* s'emet de manera incondicional ([`BR-VFC-035`](#traceability)): * Sota **IPSI** no s'emet mai. Les regles de validació de l'AEAT són explícites que aquest impost no porta `ClaveRegimen`. * Sota **IVA** i **IGIC** la clau es deriva, amb un valor general conservador, i no es fixa mai de manera rígida a `08`. El codi `08` correspon a un emissor peninsular amb una operació localitzada a Canàries, Ceuta o Melilla — no a un emissor establert allà, que declara el seu propi impost amb la seva pròpia llista. * Una causa d'exempció a nivell de document que porti la seva pròpia clau de règim especial —béns usats, agricultura, agències de viatges, criteri de caixa, recàrrec d'equivalència— té prioritat sobre aquest valor per defecte. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-INV-031` — el catàleg tancat L8.1 de `lines[].regime_key`, el seu valor derivat de la capçalera i la invariant de *huella* idèntica. * `BR-INV-032` — les causes d'exempció de línia guanyant a la qualificació derivada de la capçalera. * `BR-VFC-029` — el mapa de qualificacions des del règim d'operació de capçalera, i el fet que per aquesta via només s'assoleixen `E5` i `E2`. * `BR-VFC-035` — com es deriva `ClaveRegimen`: mai fixada de manera rígida a `08`, mai emesa per a l'IPSI, prioritat de la clau especial de la causa d'exempció. * `BR-TAX-024` — la causa d'exempció a nivell de document i la menció legal automàtica. * `BR-TAX-028` — el catàleg fiscal públic: les seves cinc fonts, la seva independència del tenant i la publicació en positiu dels tipus de retenció. --- # Abast i limitacions (/ca/guides/scope-and-limitations) Tota plataforma té fronteres. Una frontera que pots llegir abans d'integrar és una decisió de disseny; una que descobreixes en producció és un defecte. Aquesta pàgina és l'única llista canònica — cap altra guia no manté la seva. Cada fila declara l'**escenari**, el seu **estat** i el **workaround**: l'alternativa disponible avui, o una declaració explícita que no n'hi ha. Hi ha exactament dos estats, perquè una tercera categoria difusa és el que converteix pàgines com aquesta en mer ornament: * **Per disseny** — no ho construirem. L'alternativa és aquí. * **En full de ruta** — ajornat, no descartat. Verificat el **31 de juliol de 2026** contra la **v1** de l'API. Una fila amb un escenari que passi a estar suportat es retira en el mateix canvi que l'implementa, en lloc de quedar-s'hi com a limitació obsoleta. ## Limitacions verificades contra el codi [#gaps] | Escenari | Estat | Workaround | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Autofactura** — el destinatari expedeix la factura en nom del proveïdor | Per disseny | No està modelada. El proveïdor expedeix la seva pròpia factura. Si operes totes dues parts, emet-la des del compte del proveïdor. | | **Factura expedida per un tercer** | Per disseny | El camp AEAT d'expedició per tercer no s'emet. Una assessoria que opera el compte d'un client emet des d'aquell compte amb [`X-Active-Profile`](/guides/acting-on-behalf); la factura es declara com a expedida per la mateixa empresa. | | **Multidivisa** | Per disseny | El contracte v1 exposa l'euro, fix: `currency` és sempre `EUR` i no hi ha cap columna de divisa. **Filtrar un llistat per qualsevol altra divisa retorna una pàgina buida, no un error.** Factura en euros i converteix fora de Factuarea. | | **TicketBAI / Batuz (País Basc)** | Per disseny | **Cap alternativa dins de Factuarea.** Els sistemes forals bascos fan servir esquemes, certificats i endpoints diferents, i requereixen el seu propi programari certificat. Les empreses amb domicili fiscal basc reben l'avís durant l'*onboarding*. | | **Inversió del subjecte passiu, i qualsevol règim d'operació de capçalera, declarats per l'API** | Per disseny | L'`operation_regime` de capçalera és de només lectura a la v1 —ni la creació ni l'actualització no l'accepten—, així que tota factura creada per l'API neix en règim general i es qualifica `S1`. L'exempció i la no subjecció es declaren per línia amb `lines[].exemption_reason`, però **la inversió del subjecte passiu és la qualificació `S2` i no té equivalent de línia**: emet aquestes factures des del tauler. Vegeu [Clients internacionals](/guides/international-customers#map). | | **Suplerts fora de la factura emesa** — pressupostos, proformes, albarans, factures de compra, plantilles de recurrents | Per disseny | Només la factura emesa modela els suplerts. Inclou l'import com a línia ordinària al document previ, i fixa `line_type` a la factura resultant **mentre encara és un esborrany** — l'operació d'actualització l'accepta. | | **Suplerts a l'XML de Facturae i UBL** — l'import a pagar de l'XML és el total fiscal, no l'import degut | En full de ruta | La base imposable i les quotes surten correctes —el suplert queda ben exclòs—, però l'import a pagar es queda curt per aquell import i cap element de l'XML no transporta la diferència. **No remetis per [FACe](/guides/face-invoicing) una factura amb línies de suplert** mentre no es mapegi el bloc natiu de Facturae 3.2.2: factura el suplert fora d'aquell canal. | | **Suplerts a les xifres agregades de cartera** — el `pending_amount` de [`GET /v1/invoices/stats`](/api-reference/invoices/public-api.v1.invoices.stats), l'informe d'*aging* i el de deutors principals | Per disseny | Aquests agregats mesuren **volum facturat**, la mateixa magnitud que declara la declaració anual d'operacions amb tercers, i tampoc no han restat mai els cobraments parcials. Per a l'import realment degut, llegeix el `pending_amount` de cada factura, que sí que mesura contra l'import a pagar. | ## Diferències deliberades respecte d'altres plataformes [#deliberate] Són decisions de producte conscients, no buits. Cadascuna existeix perquè l'alternativa que hem triat és millor per a qui integra que el patró que se'ns demana. | Escenari | Estat | Workaround | | ---------------------------------------------------------------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paginació per desplaçament** amb recompte de pàgines i salt a la pàgina N | Per disseny | **Paginació per cursor** a l'estil de Stripe: `limit` amb límits validats, més `starting_after` **o** `ending_before` (mútuament excloents). Les respostes porten `has_more` i `next_cursor`, i `next_cursor` val `null` quan `has_more` és fals. Vegeu [Paginació](/guides/pagination). | | **Envelope d'error dual permanent** — el nostre envelope i l'RFC 9457 al mateix cos, sempre | Per disseny | **Negociació de contingut.** `Accept: application/problem+json` retorna RFC 9457 pur; qualsevol altra cosa —`application/json`, `*/*`, sense capçalera `Accept`— retorna el nostre envelope. Vegeu [Errors](/guides/errors). | | **Atomicitat total en la creació massiva** — una fila dolenta rebutja el lot sencer | Per disseny | **Èxit parcial.** La resposta porta `{dry_run, total, successful, failed, results, failures}`, on cada fallada identifica la seva fila per l'`index` que comença a zero amb el seu propi codi d'error. Importa'n 480 de 500 i arregla les 20. Vegeu [Operacions massives](/guides/bulk-operations). | | **Totals de línia obligatoris a la petició** (`line_total`, `taxable_base`) | Per disseny | `line_total` és una **suma de control opcional verificada**: es compara amb el total calculat amb una tolerància d'un cèntim i després es descarta — mai no es persisteix ni es retorna. No has de replicar el nostre motor de càlcul. Vegeu [Suplerts](/guides/disbursements#checksum). | | **Representació o apoderament per tercers** — endpoints de representació, documents d'autorització signats | Per disseny | Cada empresa puja **el seu propi certificat**, que ha de coincidir amb el seu propi NIF, es valida per estructura i mida, i té la contrasenya desada xifrada. | | **Substitució de factures simplificades en dos passos** — una rectificativa més una factura completa nova | Per disseny | **Un sol pas natiu:** `POST /v1/invoices/substitute-simplified` emet la factura substitutiva agregant diverses simplificades. Vegeu [Factures simplificades o completes](/guides/simplified-vs-full-invoices#substitute). | | **SDK de Python** | En full de ruta | Genera un client des del document OpenAPI publicat, o fes servir els SDK de [TypeScript](/sdks/typescript) o [PHP](/sdks/php), el [CLI](/cli) o el [servidor MCP](/mcp). | ## Capacitats que pots donar per absents [#capabilities] Quatre coses que Factuarea fa i que qui arriba d'altres plataformes espera rutinàriament no trobar-hi: | Capacitat | On | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Factura substitutiva de simplificades, en una sola crida** — agregar diversos tiquets en una factura completa sense una rectificativa prèvia | [Factures simplificades o completes](/guides/simplified-vs-full-invoices#substitute) · [`POST /v1/invoices/substitute-simplified`](/api-reference/invoices/public-api.v1.invoices.substitute_simplified) | | **Esmena de registres VeriFactu rebutjats, exposada a l'API pública** — reparar una declaració refusada sense anul·lar la factura | [Esmena de registres VeriFactu](/guides/verifactu-subsanacion) · [`POST /v1/verifactu/records/{id}/subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) | | **Rectificativa per diferències amb base imposable negativa** — la manera fiscalment correcta d'expressar un abonament | [Factures rectificatives](/guides/corrective-invoices#nature) | | **Catàleg fiscal de l'AEAT consultable per l'API** — règims indirectes, règims d'operació, causes d'exempció amb el seu article de la LIVA, tipus de retenció i els parells legals d'IVA i recàrrec, en tres idiomes | [Claus de règim](/guides/regime-keys#tax-catalog) · `GET /v1/tax-catalog` | Cap d'aquestes no s'anuncia ni està en desenvolupament: totes quatre són operacions vives avui. ## On viu el raonament fiscal [#see-also] Aquesta pàgina llista fronteres. Les guies que expliquen les regles que hi ha darrere: ## Traçabilitat [#traceability] Aquesta pàgina documenta l'**absència** de comportament, cosa que cap regla de negoci no pot afirmar. Les seves files, per tant, s'ancoren de manera diferent de les de les altres guies fiscals: a un punt verificat del codi, a la decisió registrada per a la plataforma, o —quan sí que existeix una regla de negoci— a aquella regla. **Limitacions verificades contra el codi:** | Fila | Ancoratge | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Autofactura | No hi ha aquesta capacitat al domini. Les úniques ocurrències del concepte són la factura que Factuarea emet als seus propis subscriptors i la comprovació de l'empresa del sistema — cap de les dues no és una capacitat de l'API. | | Factura expedida per un tercer | El camp AEAT d'expedició per tercer no s'emet mai; cap ocurrència al codi de l'aplicació. | | Multidivisa | `InvoiceV1Resource` retorna el literal `'EUR'`, i el repositori de lectura de la v1 documenta que qualsevol altra divisa dona una pàgina buida. | | TicketBAI / Batuz | `BR-VFC-019` — deliberadament fora de l'abast del context VeriFactu. | | Inversió del subjecte passiu per l'API | Cap petició de la v1 no accepta `operation_regime`; el recurs de factura el retorna de només lectura. `BR-VFC-029` deriva la qualificació d'aquell règim de capçalera, i `BR-INV-032` acota el catàleg de línia a causes d'exempció i no subjecció, sense codis `S`. | | Suplerts fora de la factura emesa | `BR-INV-037` i l'objecte de valor de tipus de línia, que declara que només la factura emesa modela els suplerts; `BR-INV-040` per a la restricció de la factura simplificada. | | Suplerts a l'XML de Facturae i UBL | L'edge case d'avís de `BR-INV-042`, que deixa registrat que l'import a pagar de tots dos documents és el total fiscal i que el bloc natiu de Facturae 3.2.2 encara no està mapejat. | | Suplerts a les xifres agregades de cartera | L'edge case de `BR-INV-045`, que deixa registrat que els agregats mesuren volum facturat i que es deixen mesurant això a propòsit. | **Diferències deliberades:** ancorades als components HTTP compartits que implementen l'alternativa —la paginació per cursor, el negociador de contingut d'error, el recurs d'èxit parcial de les operacions massives—, a `BR-INV-044` per a la suma de control de línia opcional, a `BR-VFC-003`, `BR-VFC-004`, `BR-VFC-022` i `BR-VFC-024` per als certificats propis de l'empresa, i a `BR-INV-015` i `BR-INV-016` per a la substitució en un sol pas. La fila de l'SDK de Python reflecteix una decisió registrada de planificar-lo per separat quan l'especificació s'estabilitzi: és ajornat, no descartat, que és per què el seu estat és *En full de ruta* i no *Per disseny*. **Capacitats:** cadascuna s'ancora a la ruta viva que la materialitza — `public-api.v1.invoices.substitute_simplified`, `public-api.v1.verifactu.records.subsanar` i `public-api.v1.tax-catalog.show` — més `BR-VFC-033` per a la base imposable negativa i `BR-TAX-028` per al catàleg fiscal. --- # Scopes i irreversibilitat (/ca/guides/scopes-and-irreversibility) Cada endpoint públic declara dues peces de metadata de seguretat **a l'especificació OpenAPI**: el scope exacte que enforça i si l'operació es pot desfer. Els clients (el [CLI](/cli/agents), els agents, el teu propi tooling) les llegeixen per fallar aviat — bloquejar una crida quan la key no té el scope, confirmar abans d'una acció irreversible — en lloc de descobrir el problema per un `403` o una mutació irrecuperable. ## Llegir-ho des de l'especificació [#llegir-ho-des-de-lespecificació] Cada operació a l'[especificació OpenAPI](/api/openapi) porta dues extensions personalitzades: ```json { "operationId": "public-api.v1.invoices.delete", "x-required-scope": "invoices:delete", "x-irreversible": true } ``` * **`x-required-scope`** — l'únic scope `resource:action` que l'API key ha de tenir per cridar l'operació. Present a **totes** les operacions. * **`x-irreversible`** — `true` només a les operacions que no es poden desfer. Absent (tractat com a `false`) a tota la resta. Són extensions de proveïdor `x-*`, així que un visor OpenAPI genèric pot no renderitzar-les — però qualsevol client que parsegi l'especificació (com el CLI) les llegeix directament. Genera un client des de l'especificació i heretes totes dues. ## Scopes [#scopes] Els scopes són `resource:action` (p. ex. `invoices:read`, `clients:delete`) — el mateix catàleg tancat que fan servir la REST API i les API keys. Una petició la key de la qual no té el scope retorna `403` amb `code: insufficient_scope`. Demana només els scopes que la teva integració necessita. Les operacions de control horari porten els seus propis scopes — `employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read` i `payroll_exports:read` — tots darrere el mòdul `control_horario`. Consulta [Control horari](/guides/workforce-overview). El catàleg complet — cada scope, què concedeix, i els sensibles — és a [Scopes i permisos](/mcp/scopes). Un scope **no** sempre es deriva del nom del recurs: alguns endpoints enforcen un scope diferent del que endevinaries (les descàrregues de PDF enforcen `pdfs:read`, els mètodes de pagament enforcen `invoices:read`, les transicions d'estat enforcen un scope `:transition`, les accions de VeriFactu enforcen `verifactu:*`). Llegeix sempre `x-required-scope` en lloc d'inferir-lo. Una API key pot tenir el super-scope `*`, que satisfà qualsevol `x-required-scope`. Consulta [Autenticació](/guides/authentication). ## Operacions irreversibles [#operacions-irreversibles] Una operació marcada `x-irreversible: true` **no té desfer**: esborra dades, emet un registre fiscal, transiciona un document a un estat terminal, o rota un secret. El [CLI](/cli/agents#irreversible-operations) demana una confirmació tipada abans d'executar-ne una; el teu propi tooling hauria de protegir-les igual. Aquestes són les categories que porten `x-irreversible: true`: | Categoria | Exemples | Scope | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | **Esborrats** (individuals) | `clients.delete`, `invoices.delete`, `products.delete`, `taxes.delete`, `webhook_endpoints.delete`… | `:delete` | | **Esborrats massius** | `invoices.bulk_delete`, `clients.bulk_delete`, `products.bulk_delete`, `suppliers.bulk_delete`… | `:delete` | | **Emissió / numeració fiscal** | `invoices.send`, `invoices.mark_sent`, `invoices.assign_real_number`, `invoices.corrective`, `invoices.substitute_simplified` | `invoices:send` / `invoices:write` | | **Void / anul·lació** | `invoices.void`, `invoices.annul` | `invoices:void` | | **Conversions terminals** | `quotes.convert`, `proformas.convert`, `delivery_notes.convert` | `:transition` | | **Cancel·lar / signar** | `delivery_notes.cancel`, `delivery_notes.sign`, `recurring_invoices.cancel`, `recurring_invoices.generate` | `:transition` / `:write` | | **VeriFactu (AEAT)** | `invoices.verifactu_create`, `verifactu.records.subsanar`, `verifactu.settings.update`, `verifactu.certificates.revoke` | `verifactu:write` | | **Secret / certificat** | `webhook_endpoints.rotate_secret`, `verifactu.certificates.revoke` | `webhooks:write` / `verifactu:write` | | **Oblit GDPR** | `delivery_notes.signature_audits.forget` | `delivery_notes:gdpr_forget` | | **Enviament FacturaE (B2G)** | `invoices.face_submissions.submit`, `face_submissions.cancel` | `facturae:write` | | **Segellat del tancament mensual** | `monthly_time_record_closes.seal` | `time_entries:write` | Aquesta llista és el resum llegible. La **font de veritat llegible per màquina** és `x-irreversible` a l'especificació — un client que la llegeix es manté correcte encara que el catàleg creixi. ## Ajuntant-ho tot [#ajuntant-ho-tot] Un client segur fa dues comprovacions abans d'una mutació: 1. **Scope-check** — té la key el `x-required-scope`? Si no, atura't en local (sense gastar un round trip). El CLI surt amb `4`; consulta [scope-check](/cli/agents#scope-check). 2. **Confirmació d'irreversibilitat** — és `x-irreversible` true? Si ho és, confirma abans de cridar. El CLI requereix `--confirm `; consulta [operacions irreversibles](/cli/agents#irreversible-operations). Els [SDKs](/sdks) oficials i el [CLI](/cli) fan totes dues per tu. Si generes el teu propi client des de l'especificació, cabla aquestes dues comprovacions tu mateix des de les extensions. --- # Factures simplificades o completes (/ca/guides/simplified-vs-full-invoices) La llei espanyola distingeix la **factura completa** (`F1`), que identifica el destinatari i li permet deduir l'IVA, de la **factura simplificada** (`F2`), el tiquet que el comerç lliura al taulell. Quan un client necessita després un document deduïble per un lot de tiquets, la llei preveu un tercer tipus: la **factura substitutiva** (`F3`), que agrega diverses simplificades. ## Quan aplica [#when] La factura simplificada està disponible en operacions de tipus minorista per sota d'un import legal. **Mai** no ho està en els casos de baix, i la comprovació d'admissibilitat els avalua en aquest ordre exacte — guanya el primer que casa: | Condició que la bloqueja | `reason_code` | | ----------------------------------------- | --------------------------- | | Operació intracomunitària | `intra_community` | | Inversió del subjecte passiu | `reverse_charge` | | Destinatari fora d'Espanya (exportació) | `export_operation` | | El client necessita una factura deduïble | `client_deduction_required` | | Total per damunt del topall legal absolut | `over_absolute_limit` | El topall que **el programari sí que aplica és de 3.000 € amb l'IVA inclòs** — el màxim que pot assolir qualsevol factura simplificada segons el RD 1619/2012 art. 4, sigui quin sigui el sector. Superar-lo respon `422` amb el codi de motiu `over_absolute_limit` ([`BR-INV-009`](#traceability)). El llindar general de 400 € del mateix article **no** s'aplica. Factuarea no demana a l'empresa que declari el seu sector econòmic, així que no pot saber si li correspon el límit elevat. Mantenir-se per sota de 400 € quan el teu sector no dona dret a la xifra més alta és responsabilitat fiscal de l'emissor, no pas una cosa que l'API t'hagi d'impedir. El catàleg d'impostos no canvia entre els dos tipus. A una `F1` i a una `F2` els apliquen els mateixos tipus d'IVA; el que difereix és el contingut obligatori del document, el topall d'import i la identificació del destinatari ([`BR-TAX-011`](#traceability)). ## Què envia l'API [#api] ### Pregunta abans de decidir [#eligibility] [`POST /v1/invoices/simplified-eligibility`](/api-reference/invoices/public-api.v1.invoices.simplified_eligibility), scope `invoices:read`. Pensat per a fluxos de caixa i de punt de venda que han de triar el tipus de document *abans* de crear res. ```bash curl -X POST https://api.factuarea.com/v1/invoices/simplified-eligibility \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"total": 3400, "client_country": "ES"}' ``` ```json { "data": { "can_be_simplified": false, "must_be_complete": true, "reason_code": "over_absolute_limit", "reason_message": "El importe 3.400,00 EUR supera el límite de 3.000,00 EUR para una factura simplificada.", "sector_limit": 3000 } } ``` `total` és obligatori i és l'import **amb l'IVA inclòs**. `client_id`, `client_country`, `is_intra_community`, `is_reverse_charge` i `client_requires_deductible` són entrades opcionals a les condicions de bloqueig de dalt. ### Crear una factura simplificada no és una operació de la v1 [#f2-not-in-v1] [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) no té camp `type` ni cap bandera de simplificada, i `client_id` és obligatori. **L'API pública no pot emetre una `F2`.** Tota factura creada per la v1 és una factura completa. És una frontera real, no una omissió que puguis esquivar amb un truc de payload. Si el teu punt de venda emet factures simplificades, es creen pel tauler o per la superfície de punt de venda; el que la v1 et dona sobre elles és la comprovació d'admissibilitat, la lectura i la substitució de baix. La conseqüència per al tractament d'errors: el `422` per una línia de suplert dins d'una factura simplificada és inabastable des de `POST /v1/invoices` i només s'assoleix per l'endpoint de rectificativa sobre un original simplificat — vegeu [Suplerts](/guides/disbursements). ### Substituir factures simplificades, en una sola crida [#substitute] [`POST /v1/invoices/substitute-simplified`](/api-reference/invoices/public-api.v1.invoices.substitute_simplified), scope `invoices:write`. Passes el destinatari i la llista de factures simplificades que vols agregar; reps una `F3` completa, ja emesa, amb número definitiu de sèrie: ```bash curl -X POST https://api.factuarea.com/v1/invoices/substitute-simplified \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "simplified_invoice_ids": [ "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f601", "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f602" ], "notes": "Consumos de junio" }' ``` Cada factura simplificada de la llista es valida ([`BR-INV-015`](#traceability)): la llista no pot estar buida ni tenir duplicats, cada factura ha de pertànyer a la teva empresa, cadascuna ha de ser realment una `F2`, cap no pot estar anul·lada ni cancel·lada, i cap no pot tenir ja una substitutiva. Quan falla, el `422` anomena el número de la factura que ho provoca. La `F3` neix emesa, i les seves línies són agregats: una línia per factura substituïda, descrita com la substitució d'aquest número de factura, amb quantitat un i el total brut original com a preu unitari, i **sense impost propi** — l'IVA ja es va repercutir a la factura simplificada. La substitució no anul·la els originals. Cada `F2` conserva el seu estat fiscal i simplement deixa constància que ha estat substituïda; l'objecte factura ho exposa com a `substituted_by`. Una `F3` **sí** que es pot rectificar, com qualsevol factura completa —els codis `R1`–`R4` valen igual per a una `F1` que per a una `F3`, vegeu [Factures rectificatives](/guides/corrective-invoices)—. El que una `F3` no pot ser és *substituïda*: només una `F2` pot ser objecte d'una substitució, i només una `F3` pot portar factures substituïdes ([`BR-INV-016`](#traceability)). ## Què surt al PDF [#pdf] La diferència visible és el bloc de destinatari. Una factura completa imprimeix el nom, el NIF i l'adreça del destinatari, congelats en el moment d'emetre; una de simplificada pot legítimament no tenir-ne cap, i imprimeix al seu lloc el marcador de consumidor final ([`BR-INV-024`](#traceability)). La `F3` s'imprimeix com una factura completa ordinària — un bloc de destinatari complet i una línia per cada tiquet substituït, anomenant cada número de factura substituïda. ## Què arriba a l'AEAT [#aeat] El tipus de factura viatja com el `TipoFactura` de l'AEAT al registre VeriFactu i és visible a l'objecte registre com a `invoice_type`: `F1`, `F2`, `F3`, o `R5` per a una rectificativa d'una simplificada. El registre assenyala a més si substitueix factures simplificades ([`BR-VFC-014`](#traceability)). El tipus també té conseqüències a les declaracions periòdiques ([`BR-TXR-004`](#traceability)): * Una `F3` sense NIF de destinatari aixeca un avís **no bloquejant** a la declaració trimestral d'IVA: és inusual, però legítim si el tiquet original tampoc no en tenia. * Una `F2` amb client registrat però sense NIF aixeca també un avís. * Una `F2` sense cap NIF queda **exclosa de la declaració anual d'operacions amb terceres persones** (**Modelo 347**) per norma de l'AEAT, i l'exclusió s'informa com a avís. Cap d'ells no bloqueja la generació. L'informe es produeix i els avisos es retornen al seu costat, com a llista buida quan no n'hi ha cap. Bloquejar seria un fals positiu freqüent. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-INV-009` — el topall aplicat de 3.000 €, el llindar no aplicat de 400 € i les operacions que descarten la factura simplificada. * `BR-INV-015` — substitució de factures simplificades per una `F3`, les seves validacions i les seves línies agregades. * `BR-INV-016` — només una `F3` porta factures substituïdes, i mai una llista buida. * `BR-INV-024` — el *snapshot* immutable del destinatari, i la seva absència en un tiquet de consumidor final. * `BR-TAX-011` — el catàleg d'impostos és idèntic per als dos tipus; el límit pertany a la facturació, no al catàleg. * `BR-VFC-014` — els tipus de factura de l'AEAT i com es resol el tipus. * `BR-TXR-004` — avisos no bloquejants de qualitat fiscal per a `F2` i `F3` sense NIF. Els codis de motiu del `422` citats a [Quan aplica](#when) vénen del servei de domini d'admissibilitat que materialitza `BR-INV-009`. --- # Etiquetes i camps personalitzats (/ca/guides/tags-and-custom-fields) Dos camps transversals et permeten classificar i enriquir els documents amb les teves pròpies dades de negoci: **`tags`** (etiquetes de classificació lliures per les quals pots filtrar els llistats) i **`custom_fields`** (una llista ordenada de parells tipats `{field, value}`). Tots dos s'estableixen a create/update i es retornen en cada lectura. | Camp | Forma | Límits | Filtrable | Recursos | | --------------- | --------------------------------- | ----------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- | | `tags` | array de slugs | ≤ 30, cadascun ≤ 40 caràcters | **Sí** (`?tags=`, `?tags[in]=`) | invoices, quotes, proformas, delivery\_notes, purchase\_invoices, recurring\_invoices, products | | `custom_fields` | array ordenat de `{field, value}` | ≤ 50 entrades | No | invoices, quotes, proformas, delivery\_notes, purchase\_invoices, recurring\_invoices | ## Etiquetes [#etiquetes] Un tag és un **slug en minúscula** que compleix `[a-z0-9-]` — només lletres, dígits i guions. Cada tag mesura com a màxim **40 caràcters**, i un document porta com a màxim **30 tags**. Passa'ls com un array JSON de strings en crear o actualitzar: ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "tags": ["consultoria", "cliente-vip"], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` Els tags es retornen com un array pla en cada lectura (buit `[]` quan no n'hi ha cap): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "number": "F-2026-0042", "tags": ["consultoria", "cliente-vip"] } ``` `tags` és una **substitució completa** en actualitzar: enviar `"tags": ["a"]` reemplaça tot el conjunt, no l'afegeix. Per afegir un tag, envia la llista sencera incloent-hi els existents. Per buidar-los, envia `[]`. ### Filtrar per tag [#filtrar-per-tag] Els endpoints de llistat accepten dos paràmetres de query per filtrar per tag — tria'n un: | Paràmetre | Semàntica | Exemple | | ---------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `tags` | **Coincidència exacta** amb un únic slug. | `?tags=cliente-vip` | | `tags[in]` | Llista separada per comes, semàntica **OR** — coincideix amb els documents que portin **qualsevol** dels slugs. | `?tags[in]=cliente-vip,moroso` | ```bash # Totes les factures etiquetades amb "cliente-vip" curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags=cliente-vip" # Factures etiquetades amb "cliente-vip" O "moroso" curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags[in]=cliente-vip,moroso" ``` El filtre per tag està disponible a `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices` i `recurring_invoices`. Es combina amb la resta de filtres i amb la [paginació per cursor](/ca/guides/pagination). ## Camps personalitzats [#camps-personalitzats] `custom_fields` és un **array ordenat** de parells tipats `{field, value}`, per a dades de negoci que vulguis mostrar al costat del document (centre de cost, número de comanda, codi de projecte…). Fins a **50** entrades; cada `field` és un string no buit de com a màxim **60 caràcters** i cada `value` és un string de com a màxim **500 caràcters**. L'ordre es conserva exactament tal com l'envies. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "custom_fields": [ { "field": "centro_coste", "value": "CC-2026-001" }, { "field": "numero_pedido", "value": "PO-2026-0042" } ], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` Igual que `tags`, l'array sencer és una substitució total en actualitzar, i es retorna en ordre en cada lectura (buit `[]` quan no n'hi ha cap). ## Camps personalitzats enfront de metadata [#camps-personalitzats-enfront-de-metadata] Tant `custom_fields` com `metadata` porten dades teves, però serveixen per a propòsits diferents — no facis servir el que no toca. Fes servir **`custom_fields`** per a dades de negoci ordenades i tipades que l'usuari veu al document. Fes servir **`metadata`** per a un mapa desordenat clau→valor de dades d'integració opaques (codis d'ERP, referències de la teva pròpia comptabilitat) que ningú llegeix visualment. Un document pot portar **tots dos**. | | `custom_fields` | `metadata` | | -------- | --------------------------------------- | --------------------------------- | | Forma | **Llista** ordenada de `{field, value}` | **Mapa** desordenat `key → value` | | Ordre | Es conserva | Cap | | Límit | ≤ 50 entrades | ≤ 50 claus | | Clau | `field`, 1–60 caràcters | clau del mapa | | Valor | string ≤ 500 caràcters | string ≤ 500 caràcters | | Intenció | Camps de negoci que l'usuari veu | Dades d'integració opaques | | Recursos | Els sis recursos de document | Tots els recursos | Els recursos mestres (`clients`, `suppliers`) no tenen `custom_fields` tipats — fes servir el seu `metadata` com a magatzem de camps personalitzats sense tipar. Els `products` accepten `tags` però no `custom_fields`. ## Exemples [#exemples] ```python import os, requests base = 'https://api.factuarea.com/v1' headers = {'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"} # Create an invoice with tags + custom_fields resp = requests.post(f'{base}/invoices', headers=headers, json={ 'client_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01', 'series_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02', 'issued_on': '2026-05-15', 'due_on': '2026-06-15', 'tags': ['consultoria', 'cliente-vip'], 'custom_fields': [ {'field': 'centro_coste', 'value': 'CC-2026-001'}, {'field': 'numero_pedido', 'value': 'PO-2026-0042'}, ], 'lines': [ {'description': 'Monthly service', 'quantity': 1, 'unit_price': 99.00, 'tax_rate_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03'}, ], }) resp.raise_for_status() # List invoices tagged "cliente-vip" OR "moroso" rows = requests.get(f'{base}/invoices', headers=headers, params={'tags[in]': 'cliente-vip,moroso'}).json()['data'] print(len(rows), 'matching invoices') ``` ```javascript const base = 'https://api.factuarea.com/v1'; const headers = { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }; // Create an invoice with tags + custom_fields await fetch(`${base}/invoices`, { method: 'POST', headers, body: JSON.stringify({ client_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01', series_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02', issued_on: '2026-05-15', due_on: '2026-06-15', tags: ['consultoria', 'cliente-vip'], custom_fields: [ { field: 'centro_coste', value: 'CC-2026-001' }, { field: 'numero_pedido', value: 'PO-2026-0042' }, ], lines: [ { description: 'Monthly service', quantity: 1, unit_price: 99.0, tax_rate_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03' }, ], }), }); // List invoices tagged "cliente-vip" OR "moroso" const url = new URL(`${base}/invoices`); url.searchParams.set('tags[in]', 'cliente-vip,moroso'); const { data } = await fetch(url, { headers }).then((r) => r.json()); console.log(data.length, 'matching invoices'); ``` ```bash # Create an invoice with tags + custom_fields curl -s -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "tags": ["consultoria", "cliente-vip"], "custom_fields": [ { "field": "centro_coste", "value": "CC-2026-001" }, { "field": "numero_pedido", "value": "PO-2026-0042" } ], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' | jq '{id, tags, custom_fields}' # List invoices tagged "cliente-vip" OR "moroso" curl -s -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags[in]=cliente-vip,moroso" | jq '.data | length' ``` --- # Impostos territorials — IVA, IGIC i IPSI (/ca/guides/territorial-taxes) La península i les Balears repercuteixen **IVA**. Canàries repercuteix **IGIC**. Ceuta i Melilla repercuteixen **IPSI**. Són tres impostos diferents amb tres graelles de tipus diferents, tres codis AEAT diferents i tres administracions diferents — i tractar-los com un de sol és la via per la qual una empresa canària acaba sobredeclarant el seu IVA. ## Quan aplica [#when] El règim aplicable es deriva de la zona AEAT de l'empresa emissora: península → IVA, Canàries → IGIC, Ceuta i Melilla → IPSI ([`BR-TAX-020`](#traceability), art. 1.3 RRSIF). La derivació és el **valor per defecte**, no tota la història. Com que una operació es pot localitzar en un lloc diferent d'on està establert l'emissor, un document pot sobreescriure el règim de manera explícita ([`BR-TAX-027`](#traceability)) — vegeu [Triar el règim](#override). ## Les tres graelles de tipus [#rates] Cada règim té una graella tancada de tipus legals i un tipus general ([`BR-TAX-020`](#traceability)): | Règim | Codi AEAT | Tipus legals | Tipus general | | ----- | --------- | ----------------------- | ------------- | | IVA | `01` | 0, 4, 10, 21 | 21 % | | IPSI | `02` | 0, 0,5, 1, 2, 4, 8, 10 | 8 % | | IGIC | `03` | 0, 3, 5, 7, 9,5, 15, 20 | 7 % | El 0 % és vàlid en tots tres — representa l'operació exempta. Fixa't en els codis AEAT: **l'IPSI és el `02` i l'IGIC el `03`**, no a l'inrevés. Un objecte de valor intern anterior els tenia invertits; emetre el que no toca produeix un rebuig o una declaració errònia. La graella **s'imposa en crear o editar un impost d'IGIC o d'IPSI**: un tipus fora de la graella del seu règim respon `422` llistant els tipus legals ([`BR-TAX-021`](#traceability)). Moure un impost existent a una altra zona revalida el seu tipus contra el règim nou, així que un impost al 21 % no es pot reetiquetar com a canari sense canviar abans el tipus. A l'IVA **no** se li estreny així a propòsit. El catàleg sembrat conté tipus històrics i transitoris —2 %, 5 %, 7,5 % de les mesures antiinflació— que no pertanyen a cap graella tancada, i rebutjar-los trencaria les dades existents. ## Què envia l'API [#api] ### Trobar els impostos correctes [#catalog] [`GET /v1/taxes`](/api-reference/taxes/public-api.v1.taxes.list) accepta tant `country_aeat_zone` (`peninsula`, `canarias`, `ceuta`, `melilla`) com el derivat `indirect_tax_regime` (`iva`, `igic`, `ipsi`). Són dues vistes equivalents de la mateixa dimensió: `?indirect_tax_regime=igic` equival a `?country_aeat_zone=canarias` ([`BR-TAX-026`](#traceability)). ```bash curl "https://api.factuarea.com/v1/taxes?indirect_tax_regime=igic" \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Cada impost exposa el seu `indirect_tax_regime`, derivat en només lectura de la seva zona, i `linked_surcharge_taxes_id`, el recàrrec d'equivalència legalment aparellat amb ell. Un règim desconegut al filtre degenera en una llista buida — no inventa mai resultats. Els impostos que no graven el consum —retencions, recàrrecs— porten un règim nul. ### Triar el règim d'un document [#override] `lines[].indirect_tax_regime` accepta `iva`, `igic` o `ipsi` a [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) i [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). La precedència és **sobreescriptura sobre zona** ([`BR-TAX-027`](#traceability)): Una sobreescriptura vàlida **guanya** al règim derivat de la zona de l'impost. Sense sobreescriptura → el règim es deriva de la zona, el comportament històric. Ni impost resoluble ni sobreescriptura → el *snapshot* es queda buit. No s'infereix res. S'apliquen dues guardes, totes dues amb `422`: * Un valor fora de `iva|igic|ipsi` es rebutja com a invariant de **domini**, no només de formulari — la mateixa resposta vingui de la superfície que vingui. * **Totes les línies d'un document han de compartir el mateix règim.** La sobreescriptura és de document, no de línia; barrejar dos règims respon `422` amb una violació de regla de negoci. Les línies sense règim les ignora la comprovació, així que un document que combini línies `igic` explícites amb línies sense tipar és homogeni. ```json { "lines": [ { "description": "Servicio prestado en Canarias", "quantity": 1, "unit_price": 100, "indirect_tax_regime": "igic" }, { "description": "Materiales", "quantity": 2, "unit_price": 50, "indirect_tax_regime": "igic" } ] } ``` El règim triat forma part del ***snapshot* fiscal immutable** de la línia, així que **sobreviu a la conversió**: un pressupost o una proforma convertits en factura hereten el règim que es va triar, en lloc de recalcular-lo amb la zona que tingui avui l'empresa ([`BR-TAX-023`](#traceability)). El camp germà `aeat_tax_code` es deriva sempre de l'impost i no és mai sobreescrivible; si n'arriba un valor, s'ignora. ## Què surt al PDF [#pdf] La columna d'impost i el bloc de totals mostren els tipus que es van aplicar de debò, així que una factura amb IGIC imprimeix tipus d'IGIC. El nom del règim no és un element imprès a part; es veu a través dels tipus i, quan el document la porta, de la menció legal de la seva causa d'exempció ([`BR-TAX-024`](#traceability)). Com que el *snapshot* és immutable, una empresa que traslladi després el seu domicili fiscal no canvia retroactivament els documents que ja ha emès. ## Què arriba a l'AEAT [#aeat] **Al registre VeriFactu**, el camp `Impuesto` de cada grup de desglossament es deriva del règim de la línia —`01` per a l'IVA, `02` per a l'IPSI, `03` per a l'IGIC— i no es fixa mai de manera rígida ([`BR-VFC-034`](#traceability)). Una factura mixta produeix un grup de desglossament per cada parell (tipus, règim), i la *huella* segella el conjunt. Les línies històriques sense *snapshot* de règim conserven `01`, de manera que no s'altera l'XML de factures ja declarades. La clau de règim viatja d'una altra manera: sota IPSI **no s'emet en absolut**, i sota IVA i IGIC es deriva en lloc de fixar-se de manera rígida — vegeu [Claus de règim](/guides/regime-keys#aeat). **A la declaració trimestral d'IVA**, la regla és absoluta: [`POST /v1/tax_reports/303`](/api-reference/tax-reports/public-api.v1.tax_reports.generate_303) agrega **només** les línies amb règim d'IVA ([`BR-TXR-039`](#traceability), [`BR-TXR-020`](#traceability)): * **IVA repercutit.** Una línia de qualsevol altre règim s'omet. No arriba mai a cap casella d'IVA. * **IVA suportat.** La base i la quota de les línies que no són d'IVA es resten del total de la factura, de manera que una compra purament d'IVA conserva exactament el seu import anterior, i una compra mixta hi aporta només la seva part d'IVA. L'IGIC i l'IPSI suportats **no són deduïbles** en aquesta declaració — són impostos diferents. L'IGIC es liquida davant l'Agència Tributària Canària; l'IPSI, davant l'administració local de Ceuta o Melilla. Cap dels dos no té res a veure amb la declaració estatal d'IVA. Les línies del *snapshot* s'agrupen per la clau composta **(règim, tipus)** i no només pel tipus, que és el que impedeix que una línia d'IPSI al 10 % es fusioni amb una línia d'IVA al 10 % ([`BR-TXR-038`](#traceability)). Entre les línies d'IVA s'emet tot tipus present —inclosos el 2 %, el 5 % i el 7,5 %—, així que no es perd res del fitxer de l'AEAT per caure fora dels tres habituals. ### L'avís territorial és un avís, mai un bloqueig [#warning] Una empresa de territori especial que generi la seva declaració d'IVA rep un **avís en espanyol** a la llista `warnings`, anomenant l'administració davant la qual es liquida l'impost indirecte. El fitxer es produeix igualment, només amb la part d'IVA ([`BR-TXR-040`](#traceability)). No és un `422` a propòsit. Una empresa canària pot tenir IVA perfectament legítim —vendes a la península, per exemple— i bloquejar li negaria una declaració vàlida. L'avís apareix només quan la zona és especial **i** el període conté realment operacions d'impost indirecte, i s'acumula amb els altres avisos de qualitat fiscal. La declaració anual d'operacions amb terceres persones (**Modelo 347**) es comporta d'una altra manera: **sí** que inclou les operacions d'IGIC i d'IPSI pel seu import total amb l'impost inclòs, perquè és agnòstica a quin impost indirecte s'aplica. Vegeu [Suplerts](/guides/disbursements#aeat) per al que canvia la seva base. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-TAX-020` — el règim indirecte com a objecte de valor derivat de la zona AEAT, les seves graelles de tipus legals i els seus codis AEAT. * `BR-TAX-021` — la imposició de la graella de tipus en crear o editar un impost d'IGIC o d'IPSI, i per què a l'IVA no se li estreny. * `BR-TAX-023` — el *snapshot* fiscal immutable per línia. * `BR-TAX-024` — la causa d'exempció a nivell de document i la seva menció legal. * `BR-TAX-026` — el filtratge del catàleg per zona i per règim, i el recàrrec vinculat que s'exposa. * `BR-TAX-027` — la sobreescriptura de règim per document, la seva precedència, les seves dues guardes amb `422` i la seva supervivència a la conversió. * `BR-VFC-034` — l'`Impuesto` del desglossament derivat per línia, mai fixat de manera rígida. * `BR-TXR-020` — la declaració d'IVA agrega només línies d'IVA, i no perd cap tipus d'IVA. * `BR-TXR-038` — el *snapshot* agrupa per (règim, tipus). * `BR-TXR-039` — exclusió de l'IGIC i de l'IPSI tant de l'IVA repercutit com del suportat. * `BR-TXR-040` — l'avís territorial que no bloqueja mai la generació. --- # Mode de prova i sandbox (/ca/guides/test-mode) Cada API key de Factuarea pertany a un de dos **entorns**, distingits pel seu prefix: | Prefix | Entorn | Opera sobre | Efectes externs | | ------------ | -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------- | | `fact_live_` | **live** (producció) | La teva empresa real | Reals: numeració fiscal legal, VeriFactu → AEAT, enviaments a FACe, emails a clients, webhooks sortints | | `fact_test_` | **test** (sandbox) | Una empresa *sandbox* aïllada | **Desactivats** (vegeu a sota) | El prefix és la **font de veritat** de l'entorn: una clau `fact_test_` sempre opera en mode de prova i una clau `fact_live_` sempre en producció. Cap paràmetre de la petició canvia l'entorn — el determina completament la clau amb què t'autentiques. Crea i prova sempre la teva integració primer amb una clau `fact_test_`. Un cop el teu flux funcioni d'extrem a extrem, canvia el prefix a `fact_live_` per passar a producció. La superfície de l'API és **idèntica** en tots dos entorns. ## Obtenir una clau de prova [#obtenir-una-clau-de-prova] Les claus de prova es creen des del dashboard de desenvolupadors exactament igual que les claus live, seleccionant l'entorn **Test** ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)). El secret generat té aquest aspecte: ``` fact_test_<24 alphanumeric characters> ``` Exemple: ``` fact_test_3pXnR2VbY7TcA9eFmN5z8KqW ``` Mateix format i entropia que una clau live (24 caràcters base62), mateixos scopes, mateix nivell de rate limit. L'única diferència és el prefix i allò a què apunta. Igual que amb les claus live, el secret es mostra **només un cop** en crear-lo — si el perds, l'hauràs de rotar. ## Utilitzar una clau de prova [#utilitzar-una-clau-de-prova] Envia-la a cada petició igual que una clau live, mitjançant `Authorization: Bearer` o `X-API-Key`: ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Els mateixos endpoints i operacions disponibles en live ho estan en test — no s'elimina ni se simula res. ## Aïllament de dades: l'empresa sandbox [#aïllament-de-dades-lempresa-sandbox] Una clau `fact_test_` opera sobre una **empresa sandbox** dedicada — un "bessó" tècnic de la teva empresa real, aprovisionat automàticament el primer cop que utilitzes el mode de prova, que hereta el pla de la teva empresa real perquè la limitació per funcionalitats sigui fidel. Gràcies a l'aïllament multi-tenant per empresa: * Els recursos creats amb una clau `fact_test_` **no són visibles** per a una clau `fact_live_`, i viceversa. * La numeració fiscal de prova fa servir les sèries pròpies del sandbox i **mai** consumeix ni altera la numeració correlativa de les teves sèries de producció. Això és aïllament estructural, no un filtre: les dades de test i live viuen en empreses separades, així que no hi ha manera que es barregin. ## Què està desactivat en test [#què-està-desactivat-en-test] Quan operes amb una clau `fact_test_` (entorn sandbox), els efectes que arriben al món exterior estan **deshabilitats** perquè puguis exercitar la teva integració sense conseqüències en el món real: | Efecte | En `live` | En `test` | | ------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **VeriFactu** | El registre d'Alta es crea i es transmet a l'AEAT. | El registre d'Alta es crea **localment**, però **mai es transmet a l'AEAT**. | | **Email** | Els emails de documents s'entreguen a destinataris reals. | Els emails de documents **no s'entreguen** a destinataris reals. | | **Webhooks** | Els esdeveniments subscrits s'entreguen als teus endpoints HTTP externs. | Els esdeveniments es registren amb `livemode: false` (consultables via `GET /v1/events`) però **no s'entreguen** als teus endpoints. | | **FACe (FacturaE)** | Els enviaments es presenten al web service real de FACe. | Tot el flux es **simula** — cap crida SOAP surt de Factuarea i el número de registre és sintètic (`FACE-SANDBOX-*`). Consulta [Facturació FACe](/guides/face-invoicing#sandbox). | Tota la resta es comporta de manera idèntica: validació, totals, màquines d'estat de documents, idempotència, paginació, rate limits i embolcalls d'error són els mateixos que en producció. Com que els webhooks no s'entreguen en test, no pots exercitar el teu receptor de webhooks contra dades del sandbox. Prova la verificació de signatura del teu endpoint amb el `POST /v1/webhook_endpoints/{id}/ping` dedicat (que sí s'entrega) o contra una clau live en un esdeveniment controlat. ## Amb els SDK oficials [#amb-els-sdk-oficials] Els [SDK de TypeScript i PHP](/sdks) segueixen la mateixa regla: **el prefix de la clau selecciona l'entorn** — no hi ha cap flag. Crea la teva integració amb una clau `fact_test_` i després canvia la variable d'entorn a `fact_live_` per passar a producció. Sense canvis de codi. ```ts import { Factuarea } from "@factuarea/sdk"; const sandbox = new Factuarea({ apiKey: "fact_test_…" }); sandbox.environment; // "test" const prod = new Factuarea({ apiKey: "fact_live_…" }); prod.environment; // "live" ``` L'SDK exposa l'entorn resolt a `.environment`, derivat del prefix — útil per a guardes i logging. ```php use Factuarea\Sdk\Custom\FactuareaClient; $sandbox = FactuareaClient::create('fact_test_…'); // sandbox $prod = FactuareaClient::create('fact_live_…'); // production ``` Els webhooks segueixen **sense entregar-se** en test, fins i tot a través de l'SDK. Per exercitar el [verificador de l'SDK](/sdks#verifying-webhooks) del teu receptor al sandbox, fes servir `POST /v1/webhook_endpoints/{id}/ping`, que *sí* s'entrega. ## Canviar d'entorn a l'app [#canviar-dentorn-a-lapp] Més enllà de les API keys, l'app web de Factuarea et permet alternar entre **live** i **test** en qualsevol moment des de la barra superior. En canviar a test: * Es reemet la teva sessió **sense tornar a iniciar sessió**, apuntant-la a l'empresa sandbox (aprovisionant-la si encara no existeix). El token anterior s'invalida. * Es mostra un banner persistent **"MODO TEST"** a tota la interfície perquè el context actiu sigui sempre evident. * Es rehidrata l'estat del client perquè els llistats reflecteixin les dades del sandbox i mai mostrin dades de producció en memòria cau. Tornar a live reemet la sessió contra la teva empresa real. El sandbox no es mostra mai com una empresa real al selector d'empreses — existeix únicament per donar suport a l'entorn de prova. ## De test a producció [#de-test-a-producció] Quan la teva integració funcioni contra `fact_test_`: 1. Crea una clau `fact_live_` al dashboard (els mateixos scopes que vas validar en test). 2. Canvia la clau que fa servir el teu client (variable d'entorn / gestor de secrets). 3. No cal cap canvi de codi — la forma de la petició és idèntica. A partir d'aquest moment, els efectes reals (numeració fiscal, VeriFactu → AEAT, emails, webhooks) tornen a estar actius. --- # Fitxatges (/ca/guides/time-clock) Fitxar escriu en un **ledger de només apèndix**: fitxar entrada, pausar, reprendre i fitxar sortida apendixen cadascun una entrada nova que mai s'edita ni s'esborra. Cada entrada s'encadena a l'anterior amb una **empremta SHA-256** ([cadena per empresa](/guides/workforce-overview)), de manera que qualsevol manipulació és detectable. L'**estat de la jornada en viu** —`not_started`, `working`, `paused`, `finished`— es **deriva** del ledger, no es desa en una columna. Tots els endpoints viuen sota `https://api.factuarea.com/v1` i usen els scopes `time_entries:read` / `time_entries:write`, el mateix [embolcall d'error](/guides/errors) i [paginació per cursor](/guides/pagination) que la resta de l'API. ## Fitxar entrada, pausar, reprendre, sortir [#clocking] Quatre operacions d'escriptura governen la jornada. Cadascuna accepta un `occurred_at` opcional (per defecte, ara) i un `source`, i retorna l'entrada apendixada. | Operació | Endpoint | Des d'estat | | ----------------- | --------------------------------- | -------------------------- | | Fitxar entrada | `POST /v1/time-entries/clock-in` | `not_started` o `finished` | | Iniciar una pausa | `POST /v1/time-entries/pause` | `working` | | Reprendre | `POST /v1/time-entries/resume` | `paused` | | Fitxar sortida | `POST /v1/time-entries/clock-out` | `working` o `paused` | Dues regles regeixen la seqüència. **Una jornada oberta alhora**: fitxar entrada dos cops retorna `422` ("Ya has fichado la entrada."); pausar o fitxar sortida sense jornada oberta retorna `422`. **Cronologia monòtona**: un `occurred_at` anterior a l'últim esdeveniment de la franja es rebutja amb `422`. Una jornada pot tenir **diverses franges** (jornada partida) — tornar a fitxar entrada després de la sortida obre una franja nova. ```bash curl -X POST https://api.factuarea.com/v1/time-entries/clock-in \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "web" }' ``` Llegeix la sessió oberta actual amb `GET /v1/time-entries/current`, llista entrades amb `GET /v1/time-entries` i obtén-ne una amb `GET /v1/time-entries/{time_entry}` — tot sota `time_entries:read`. Consulta els esquemes a la [Referència d'API](/api-reference/time-entries/public-api.v1.time_entries.clock_in). ## Fitxatges retroactius (manuals) [#manual] `POST /v1/time-entries/manual` registra una **franja passada completa** (entrada, pauses opcionals i sortida) per a un empleat que va oblidar fitxar. El `reason` és **obligatori** — es segella a la cadena d'empremtes com a part de l'evidència — i l'entrada es marca `is_retroactive` amb `source: manual`. A diferència del fitxatge live self-service, un fitxatge manual és una acció privilegiada i es registra a l'audit log. ```bash curl -X POST https://api.factuarea.com/v1/time-entries/manual \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "started_at": "2026-02-03T09:00:00+01:00", "ended_at": "2026-02-03T17:00:00+01:00", "reason": "Va oblidar fitxar; confirmat pel responsable" }' ``` ## El flux de correccions [#corrections] Un registre de jornada **mai** s'edita. Per esmenar un error, un empleat obre una **sol·licitud de correcció**; un manager o admin l'aprova o la rebutja. Una sol·licitud passa `pending → approved` o `pending → rejected`, tots dos terminals. | Operació | Endpoint | Efecte | | ------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | Sol·licitar una correcció | `POST /v1/time-corrections` | Crea una sol·licitud `pending`. | | Aprovar | `POST /v1/time-corrections/{correction}/approve` | Apendixa una entrada de correcció a l'original; emet `time_entry.corrected`. | | Rebutjar | `POST /v1/time-corrections/{correction}/reject` | Registra un rebuig amb motiu; l'original queda intacte. | | Llistar / detall | `GET /v1/time-corrections`, `GET /v1/time-corrections/{correction}` | Llegeix l'estat del flux. | L'aprovació **apendixa una entrada nova** que referencia l'original — l'error i la seva esmena queden tots dos al ledger. Dues guardes apliquen: **no pots aprovar la teva pròpia** sol·licitud (`422`), i una sol·licitud ja resolta no es resol de nou (`422`). ```bash curl -X POST https://api.factuarea.com/v1/time-corrections/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/approve \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "note": "Verificat amb el registre de fitxatges" }' ``` No hi ha actualització ni esborrat per a un registre de jornada. Cada correcció és una **entrada nova** que manté l'original intacte — això és el que fa el registre defensable davant la Inspecció de Treball. ## Verificar la integritat de la cadena [#chain] `GET /v1/time-entries/chain/validate` recalcula tota la cadena d'empremtes i informa de si està intacta, retornant l'id de la primera entrada trencada si n'hi ha. És una comprovació d'integritat de **només lectura** (amb límit de taxa) — fes-la servir per demostrar que el registre no ha estat alterat. ## Pròxims passos [#next] * [Horaris](/guides/work-schedules) — les hores esperades contra les quals es mesura el ledger. * [Tancament mensual](/guides/monthly-time-close) — congelar i segellar un mes finalitzat. * Explora la referència de [fitxatges](/api-reference/time-entries/public-api.v1.time_entries.list) i [correccions](/api-reference/time-corrections/public-api.v1.time_corrections.create). --- # Alta automàtica a VeriFactu (/ca/guides/verifactu-auto-submission) Qui integra venint d'altres plataformes de facturació busca l'operació que envia una factura a l'Administració tributària, no la troba i dona per fet que la funcionalitat falta. No falta: **l'alta no és un pas que executis tu.** El registre es crea com a conseqüència d'emetre la factura, i el transmet una canonada en segon pla. Aquesta pàgina respon a «per què la meva factura no ha arribat a l'AEAT?», que gairebé sempre és una de les comportes de baix i no una fallada. ## Quan aplica [#when] A tota factura que surt de `draft` en una empresa amb l'activació de VeriFactu efectiva. En concret, l'alta es crea en la transició a `sent` — incloses les factures que neixen ja emeses: rectificatives, substitutives `F3`, generacions de recurrents i creacions que passen `status: sent` directament. L'etapa `draft` queda deliberadament fora del mecanisme. Un esborrany no té número definitiu, ni *snapshot* congelat del destinatari, ni existència fiscal; no se'n declara res. ## Les comportes, en l'ordre en què s'avaluen [#gates] **Interruptor d'emergència de la instància.** Una bandera global pot desactivar VeriFactu per a tota la instal·lació. És un interruptor d'emergència, mai una activació: tota sola no habilita res. **Activació per empresa.** Aquesta és la que controles tu. Ve **desactivada** de fàbrica en un compte acabat de crear — una empresa nova *no* dona d'alta les seves factures fins que algú activa VeriFactu. L'activació efectiva és `instància I empresa` ([`BR-VFC-025`](#traceability)). Llegeix-la amb [`GET /v1/verifactu/config`](/api-reference/verifactu/public-api.v1.verifactu.config): el camp `enabled` ja és el valor efectiu, no la bandera crua de l'empresa. **Mode de funcionament.** Amb l'activació posada, l'empresa encara tria entre transmetre i no transmetre. En mode `no_verifactu` els registres encadenats es continuen generant i desant en local — el mode canvia la transmissió, no l'encadenament — i han de quedar disponibles per a inspecció, però no s'envia res en temps real ([`BR-VFC-018`](#traceability), RD 1007/2023 art. 16). **Excepció de la importació històrica.** Les factures carregades per la importació massiva d'històric previ a l'adhesió porten una marca transitòria que fa que els gestors de VeriFactu retornin sense crear cap registre ([`BR-INV-011`](#traceability), [`BR-VFC-009`](#traceability)). Sense ella, importar anys d'històric declararia milers d'altes amb dates d'expedició anteriors a la incorporació de l'empresa al sistema. La marca la força l'importador i **no** s'exposa als endpoints ordinaris de creació — no la pots activar des de l'API pública. **Certificat actiu.** Signar requereix el certificat FNMT propi de l'empresa. Si no n'hi ha cap, o està caducat, revocat, o el seu NIF no coincideix amb el de l'empresa, la creació de l'alta falla amb un error de regla de negoci. Comprova `has_active_certificate` a l'endpoint de configuració abans de sortir a producció. Si passen les cinc, el registre es crea, s'encadena i s'encua per transmetre. Que la cua transmeti automàticament és al seu torn un ajust d'instància, exposat en només lectura com a `auto_transmit` a l'endpoint de configuració. ## Què envia l'API [#api] Res que escriguis tu. No hi ha cos de petició per a «enviar», ni cap camp a [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) que ho controli. El que sí que controles és *quan s'emet la factura*, i d'aquí se'n deriva tota la resta: * Crea la factura com a esborrany i després emet-la amb [`POST /v1/invoices/{id}/send`](/api-reference/invoices/public-api.v1.invoices.send) o [`POST /v1/invoices/{id}/mark-sent`](/api-reference/invoices/public-api.v1.invoices.mark_sent). * O crea-la i emet-la de manera atòmica passant `options.issue_directly` a la crida de creació. Com que el camí de «crear i emetre en una sola crida» emet alhora un esdeveniment de creació i un d'emissió, dos gestors competeixen per crear la mateixa alta. La comanda és **idempotent per factura**: el segon detecta l'alta existent i no fa res en silenci, de manera que existeix exactament un registre per factura ([`BR-VFC-008`](#traceability)). No cal que dedupliquis pel teu costat. ### L'única via d'escapament explícita [#force] *Sí* que existeix una operació que força la creació d'una alta per a una factura ja emesa: [`POST /v1/invoices/{id}/verifactu`](/api-reference/verifactu/public-api.v1.invoices.verifactu_create), scope `verifactu:write`. Crea l'alta i encua la seva transmissió, responent `201` amb el registre nou. Existeix per al cas en què una factura es va emetre amb una comporta tancada —un certificat que encara no s'havia pujat, per exemple— i vols l'alta així que la comporta s'obre. **No** és un reenviament: | Situació | Resposta | | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | La factura ja té una alta | `422` `verifactu_already_submitted` | | La factura continua en esborrany, VeriFactu està desactivat a la instància, o el certificat falta, està caducat, revocat o amb un NIF que no casa | `422` `verifactu_not_eligible` | | La factura no existeix, o pertany a una altra empresa | `404` `invoice_not_found` | ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ### Les úniques palanques manuals sobre un registre que ja existeix [#levers] Creada l'alta, exactament dues operacions hi actuen, i totes dues s'expliquen a [Estats d'enviament VeriFactu](/guides/verifactu-submission-states): * [`retry`](/api-reference/verifactu/public-api.v1.verifactu.records.retry) — reenvia sense canvis la declaració emmagatzemada, per a fallades tècniques. * [`subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) — regenera la declaració a partir de dades mestres corregides, per a rebutjos de l'AEAT. No existeix cap operació que retransmeti un registre acceptat. L'acceptació és terminal per norma. ## Activar és un compromís, no un interruptor [#commitment] Encendre VeriFactu és asimètric, i una integració que ho tracti com un interruptor reversible ensopegarà amb un `422` en producció. Passar al mode verificable sempre està permès. **Tornar enrere** està bloquejat fins al 31 de desembre de l'any en què es va activar ([`BR-VFC-001`](#traceability), [`BR-VFC-023`](#traceability), RD 1007/2023 art. 13). La integritat d'una cadena declarada a l'AEAT en temps real no es pot degradar a programari autocertificat a mitjan exercici fiscal. Hi ha una escapatòria deliberada: mentre la cadena continuï **buida** —l'empresa no ha emès ni un sol registre de facturació en cap estat— l'empresa pot canviar d'idea i tornar enrere, i el bloqueig s'aixeca. El primer registre emès, encara que sigui un de rebutjat o amb error, arma el bloqueig fins a final d'any. Apagar la bandera d'activació per empresa ho impedeix la mateixa guarda, així que no serveix per esquivar el compromís. `GET /v1/verifactu/config` exposa `is_locked_until` perquè ho puguis ensenyar als teus usuaris abans que es comprometin. ## Sandbox i producció no són intercanviables [#environments] Cada empresa opera contra un únic entorn AEAT, exposat com a `environment` tant a l'objecte de configuració com a cada registre. Un CSV obtingut contra l'entorn de proves de l'AEAT **no** és una alta: els CSV de proves porten un prefix recognoscible, i una base de dades de producció que en contingui significa que es va simular alguna cosa que s'hauria d'haver transmès ([`BR-VFC-017`](#traceability)). La regla que et protegeix és que el sistema no ha de caure mai en simulació de manera silenciosa — un endpoint inaccessible ha d'aflorar com a estat tècnic `error`, no com una acceptació fabricada. Quan concilies, tracta el camp `environment` com a part de la identitat del registre. ## Què surt al PDF [#pdf] L'alta automàtica en si no afegeix res al document; el que s'imprimeix depèn de l'*existència* d'un registre, no de com es va crear. Així que existeix un registre, la factura porta el bloc QR legal ([`BR-VFC-015`](#traceability)), i la llegenda sota el codi difereix segons el mode de funcionament: la marca curta `VERI*FACTU` en mode verificable, i la frase completa que declara que la factura és verificable a la seu electrònica de l'AEAT en l'altre. Una factura importada amb l'excepció d'històric no té registre i per tant **no imprimeix QR**. És el que toca: les factures anteriors a l'adhesió no són verificables a l'AEAT. ## Què arriba a l'AEAT [#aeat] Una declaració d'alta per factura emesa, encadenada al registre anterior de l'empresa, més una declaració d'anul·lació si la factura s'anul·la després (vegeu [Anul·lar o rectificar](/guides/annul-vs-correct)). No es transmet res més com a conseqüència d'emetre. En mode `no_verifactu` no arriba res a l'AEAT en temps real; l'empresa conserva la cadena local per a inspecció i el sistema registra periòdicament resums dels seus propis esdeveniments operatius, que la norma tracta com a evidència separada ([`BR-VFC-018`](#traceability)). ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-VFC-001` — l'adhesió al mode verificable és irrevocable fins a final d'any natural, amb l'excepció de la cadena buida. * `BR-VFC-008` — idempotència: una alta per factura, fins i tot quan el flux de crear i emetre dispara dos esdeveniments. * `BR-VFC-009` — l'excepció de la importació històrica, vista des de VeriFactu. * `BR-VFC-015` — el bloc QR i les seves dues llegendes. * `BR-VFC-017` — la frontera entre sandbox i producció, i la prohibició de simular en silenci. * `BR-VFC-018` — mode `no_verifactu`: cadena local, resums d'esdeveniments, sense transmissió en temps real. * `BR-VFC-023` — el canvi de mode asimètric i el bloqueig fins a final d'any, inclosa la guarda que impedeix esquivar-lo amb la bandera d'activació. * `BR-VFC-025` — activació per empresa, desactivada de fàbrica, valor efectiu com a `instància I empresa`. * `BR-INV-011` — l'excepció de la importació històrica, vista des de la facturació. --- # Estats d'enviament VeriFactu (/ca/guides/verifactu-submission-states) Cada factura que emet la teva empresa sota VeriFactu produeix un **registre de facturació**: una declaració XML signada que es transmet a l'AEAT i queda encadenada criptogràficament al registre anterior de la mateixa empresa. La factura i el seu registre són dos objectes diferents amb dos cicles de vida diferents — una factura pot estar `sent` i cobrada mentre el seu registre continua `rejected` per l'AEAT. Aquesta pàgina tracta del **registre**. Si integres contra Factuarea i només vigiles l'estat de la factura, no t'assabentaràs que l'Administració tributària ha rebutjat una declaració. ## Quan aplica [#when] El cicle de vida del registre aplica a tota empresa amb VeriFactu efectivament activat, des del moment en què una factura surt de `draft`. **No** aplica a: * Empreses encara en mode `no_verifactu`: els registres es continuen creant i encadenant en local, però no es transmeten mai, de manera que queden fora del cicle acceptat/rebutjat ([`BR-VFC-018`](#traceability), RD 1007/2023 art. 16). * Factures històriques importades saltant-se el pas de VeriFactu — no es crea cap registre, així que no hi ha res a consultar ([`BR-VFC-009`](#traceability)). Consulta [Alta automàtica a VeriFactu](/guides/verifactu-auto-submission) per a les comportes que decideixen si el registre arriba a crear-se. ## Els cinc estats [#states] | `status` | Significat | Terminal? | Què fas | | ----------- | ------------------------------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------ | | `pending` | El registre existeix i està encadenat, però encara no s'ha transmès. | No | Res. La transmissió està a la cua. | | `submitted` | Enviat a l'AEAT, a l'espera de la resposta definitiva. | No | Res. Consultar. | | `accepted` | L'AEAT ha registrat la declaració. `aeat_csv` ve informat. | **Sí — immutable** | Res. Per corregir la factura, emet una [rectificativa](/guides/corrective-invoices). | | `rejected` | L'AEAT l'ha rebutjat per un error de **dades** (NIF del destinatari desconegut, esquema, totals). | Resposta definitiva, però reparable | Corregeix les dades i després `subsanar`. | | `error` | Fallada **tècnica** de transmissió: timeout, AEAT inaccessible, problema de signatura. | No | Res, o forçar un `retry`. | La distinció que importa és `rejected` davant `error`. `rejected` és l'AEAT dient «he llegit la teva declaració i està malament». `error` és la declaració que no va arribar mai. Es reparen amb operacions diferents, i confondre-les és l'error d'integració més habitual en aquest endpoint. `accepted` és l'únic estat genuïnament immutable: la matriu de transicions rebutja qualsevol sortida d'aquest estat, perquè el RD 1007/2023 fa inalterable un registre ja registrat. Tots els altres estats admeten una transició nova, i això és el que fa possibles el reintent i l'esmena. ## Què envia l'API [#api] No crees mai un registre amb un payload — es crea per tu. El que fas és llegir-lo. L'API v1 retorna l'objecte registre a [`GET /v1/verifactu/records/{id}`](/api-reference/verifactu/public-api.v1.verifactu.records.show), [`GET /v1/verifactu/records`](/api-reference/verifactu/public-api.v1.verifactu.records.list) i, indexat per factura, a [`GET /v1/invoices/{id}/verifactu`](/api-reference/verifactu/public-api.v1.invoices.verifactu_get): | Camp | Significat | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `status` | Un dels cinc estats de dalt. | | `type` | `ALTA` (la factura es va emetre) o `ANULACION` (es va anul·lar). | | `invoice_type` | El tipus de factura AEAT congelat en el moment d'emetre: `F1`, `F2`, `F3`, `R1`–`R5`. | | `huella` | La *huella* SHA-256 d'**aquest** registre, en hexadecimal majúscules. És la baula a què apuntarà el registre següent. | | `aeat_csv` | El *Código Seguro de Verificación* que retorna l'AEAT en acceptar. Val `null` fins llavors. És el valor amb què concilies contra l'Administració tributària. | | `aeat_submission_id` | El nostre identificador de transmissió, per a converses amb suport. | | `transmitted_at` | ISO 8601 de l'última transmissió que va arribar a l'AEAT. Ve informat en `submitted` i `accepted`; val `null` en `pending`, `rejected` i `error`. | | `environment` | L'entorn AEAT **d'aquesta empresa** — producció o l'entorn de proves de l'AEAT. Un CSV obtingut en proves no és una alta real. | | `is_simplificada` / `is_substitute_for_simplified` | Si la factura d'origen era una `F2`, i si aquest registre substitueix factures simplificades mitjançant una `F3`. | Dos camps mereixen el seu propi avís. **La `huella` és identitat, no una suma de control que puguis recalcular.** Es calcula a partir del NIF de l'emissor, la sèrie i el número, la data d'expedició, el tipus de factura, la quota total, l'import total, la *huella* del registre *anterior* i la marca de temps de generació — en aquest ordre i format exactes. Si qualsevol d'aquests valors canvia, la cadena es trenca i falla la prova d'integritat de tota l'empresa ([`BR-VFC-013`](#traceability)). Per això hi ha correccions que no es poden reparar al lloc; vegeu [Reintentar o esmenar](#retry-vs-subsanar). **L'`aeat_csv` s'escriu una sola vegada.** En acceptar-se es persisteix i no se sobreescriu mai, ni tan sols si l'AEAT retorna el mateix CSV en una transmissió posterior. Un registre que passa a `rejected` després d'haver estat `submitted` conserva el CSV anterior a efectes d'auditoria, així que un `aeat_csv` no nul en un registre `rejected` és el que s'espera, no una fallada ([`BR-VFC-016`](#traceability)). ```bash curl https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ```json { "data": { "id": "0197b3c9-1de2-7c40-8b71-a4d5e6f70123", "object": "verifactu_record", "type": "ALTA", "invoice_type": "F1", "invoice_number": "F-2026-0042", "date": "2026-05-15", "amount": 121.0, "status": "accepted", "huella": "9F2C1A0B7E4D6835A1C0B9E8D7F6A5B43C2D1E0F9A8B7C6D5E4F3A2B1C0D9E8F", "aeat_submission_id": "sub_0197b3c9", "aeat_csv": "FCT-2026-A1B2C3D4-E5F6", "environment": "production", "transmitted_at": "2026-05-15T09:41:02Z", "is_simplificada": false, "is_substitute_for_simplified": false, "created_at": "2026-05-15T09:40:58Z" } } ``` ### El pressupost de reintents existeix, i no viatja al payload [#retry-budget] Darrere d'`error` hi ha un comptador i una planificació. Una transmissió fallida es torna a encuar amb retrocés exponencial, i el nombre de reintents tècnics a cegues està limitat per ronda de transmissió; esgotat el límit, un reintent manual addicional respon amb un error de regla de negoci en lloc de tornar a encuar ([`BR-VFC-006`](#traceability)). Al costat del comptador, el registre porta una marca d'incidència tècnica, que s'aixeca quan va ser la mateixa AEAT la que va estar inaccessible i es va haver de declarar la incidència — es conserva fins i tot després d'una acceptació posterior, a efectes d'auditoria. **Cap d'aquests tres valors — el comptador d'intents, el reintent programat següent i la marca d'incidència — no s'exposa a l'objecte registre de la v1.** Governen el comportament que observes, però avui no els pots llegir per l'API pública. El que *sí* que pots observar és l'estat mateix, `transmitted_at` i la cronologia d'auditoria del registre via [`GET /v1/verifactu/records/{id}/activities`](/api-reference/verifactu/public-api.v1.verifactu.records.activities). No construeixis al teu client un model endevinat de la planificació de reintents: consulta l'estat. ## Reintentar o esmenar [#retry-vs-subsanar] Totes dues operacions actuen sobre un registre que ja existeix. No són intercanviables. | Estat del registre | Causa | Operació | Per què | | --------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `error` | La declaració no va arribar mai a l'AEAT. | [`POST /v1/verifactu/records/{id}/retry`](/api-reference/verifactu/public-api.v1.verifactu.records.retry) | L'XML emmagatzemat és correcte. Es reenvia sense canvis. | | `rejected` | L'AEAT el va llegir i va rebutjar les dades. | [`POST /v1/verifactu/records/{id}/subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) | Cal regenerar l'XML a partir de les dades mestres corregides. | | `accepted` | — | Cap. | El registre és immutable. Emet una [factura rectificativa](/guides/corrective-invoices). | | `rejected`, però la correcció toca un camp de la *huella* | Es va declarar malament el total, la data, el número, el NIF o el tipus de factura. | Cap — anul·lar i tornar a emetre. | Canviar un camp de la *huella* invalidaria la cadena. `subsanar` ho rebutja d'entrada. | Reintentar a cegues consumeix el pressupost de reintents tècnics. L'esmena no: és una correcció manual deliberada amb dades noves, la norma no li posa límit i executar-la **reinicia la ronda de transmissió** — el comptador d'intents torna a zero i la retransmissió automàtica del contingut corregit recupera el pressupost íntegre ([`BR-VFC-006`](#traceability), [`BR-VFC-020`](#traceability)). L'esmena regenera el payload però **hi incrusta la *huella* original, la baula de cadena original i la marca de temps de generació original**, perquè són les que permeten a l'AEAT casar el reenviament amb el registre que va rebutjar. Abans de persistir res compara els camps regenerats que entren a la *huella* amb els emmagatzemats; si algun difereix, respon `422` amb el subcodi que t'indica que cal anul·lar, i no es modifica res. El flux complet, els subcodis d'error i els consells de prevenció són a [Esmena de registres VeriFactu](/guides/verifactu-subsanacion). ## Què surt al PDF [#pdf] L'estat del registre **no** canvia el PDF. Sigui quin sigui l'estat, la factura imprimeix el mateix bloc QR legal a la cantonada superior dreta de la primera pàgina: l'etiqueta `QR tributario:`, un codi de 30×30 mm que apunta al servei de verificació de l'AEAT amb el NIF de l'emissor, la sèrie i el número, la data i el total, i la llegenda a sota ([`BR-VFC-015`](#traceability)). Tres conseqüències que convé contemplar en el disseny: * El QR s'imprimeix així que existeix un registre — fins i tot mentre està `pending`, `error` o `rejected`. Un destinatari que l'escanegi abans de l'acceptació veurà que l'AEAT no informa de cap alta. És el comportament correcte, no un defecte. * El CSV **no** s'imprimeix al PDF. Només està disponible per l'API i al tauler. * La *huella* i la marca de temps de l'alta també van deixar d'imprimir-se. Si els estaves extraient del PDF, llegeix-los del registre. L'esmena és l'única operació que a més toca el document imprès: torna a congelar deliberadament els *snapshots* immutables de destinatari i emissor a partir de les dades mestres actuals, perquè el PDF coincideixi amb el que es va tornar a declarar a l'AEAT ([`BR-INV-024`](#traceability), [`BR-VFC-020`](#traceability)). És l'únic camí que reescriu un *snapshot* ja congelat. ## Què arriba a l'AEAT [#aeat] Cada registre transmet una declaració, encadenada per la seva *huella* al registre anterior de la mateixa empresa. Existeixen tres classes de registre, i no comparteixen una sola cadena: les altes (`ALTA`) i les anul·lacions (`ANULACION`) comparteixen la cadena de facturació, mentre que els registres d'esdeveniments del sistema mantenen una cadena pròpia a part, perquè la norma tracta els esdeveniments operatius com a evidència separada ([`BR-VFC-014`](#traceability)). Quan un reenviament segueix un rebuig, la declaració regenerada porta a més les marques AEAT que declaren que l'enviament anterior va ser rebutjat i que, per tant, el registre no va arribar mai a registrar-se. Cap de les dues no entra al càlcul de la *huella*, així que declarar-les no pertorba la cadena ([`BR-VFC-026`](#traceability)). Pots verificar la cadena sencera pel teu compte amb [`GET /v1/verifactu/chain/validate`](/api-reference/verifactu/public-api.v1.verifactu.chain.validate), que recalcula totes les *huellas* i informa de les anomalies. Està limitat a una crida per minut i empresa perquè recorre el llibre registre complet. ## Traçabilitat [#traceability] Derivat de les regles de domini del backend de Factuarea: * `BR-VFC-006` — política de reintents: retrocés exponencial, intents limitats per ronda, i el límit que explícitament no aplica a l'esmena. * `BR-VFC-013` — la cadena de *huellas* és immutable i verificable; qualsevol alteració invalida la garantia d'integritat davant l'AEAT. * `BR-VFC-014` — les tres classes de registre i les seves cadenes independents. * `BR-VFC-015` — el bloc QR obligatori a la factura impresa. * `BR-VFC-016` — el CSV com a identitat pública del registre, persistit sense alterar. * `BR-VFC-018` — mode `no_verifactu`: cadena local, sense transmissió. * `BR-VFC-020` — esmena de registres rebutjats, guarda de la *huella* i reinici de la ronda de transmissió. * `BR-VFC-026` — les marques AEAT per a un reenviament després d'un rebuig. * `BR-INV-024` — el *snapshot* immutable del destinatari i l'única excepció que el refresca. Derivat també de la màquina d'estats de transmissió documentada al costat d'aquestes regles (`AeatTransmissionStatus`), que és la font de veritat de la matriu de transicions citada a [Els cinc estats](#states). --- # Esmena de registres VeriFactu (/ca/guides/verifactu-subsanacion) Quan l'AEAT rebutja un registre de facturació VeriFactu per un **error de dades** (registre amb `status: rejected`), la normativa VeriFactu (Reial decret 1007/2023, art. 11) et permet **esmenar** (*subsanar*) el registre: reenviar el **mateix registre** amb el contingut corregit. No és una factura nova, ni una rectificativa, ni una anul·lació — el mateix registre rebutjat es repara i es transmet de nou. ``` POST /v1/verifactu/records/{record}/subsanar ``` * **Scope:** `verifactu:write` * **Body de la request:** cap — el contingut esmenable es regenera al servidor des de la factura origen i les dades mestres **actuals**. * **Resposta:** `202 Accepted` — el reenviament s'encua i s'envia a l'AEAT en uns segons. L'esmena **mai no recalcula** la *huella* original, la baula d'encadenament amb el registre anterior ni el segell de generació original: l'AEAT casa el reenviament amb el registre rebutjat precisament perquè es conserven. **No hi ha límit d'intents d'esmena** — el límit de reintents tècnic aplica només als reintents cecs de fallades de transmissió. ## Quan fer-la servir — i quan no [#when] | Situació | Què fer | | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Registre **`rejected`** per l'AEAT per un error de dades — NIF del destinatari no identificat al cens, raó social incorrecta, problemes a la descripció… | **Esmena.** Corregeix les dades al seu origen i crida `POST …/subsanar`. | | Registre en **`error`** (fallada tècnica de transmissió: timeout, AEAT caiguda). | Res — la transmissió es reintenta automàticament ([reintent manual](/api-reference/verifactu/public-api.v1.verifactu.records.retry) disponible). L'esmena respon `record_not_rejected`. | | Registre **`accepted`** però la factura porta dades incorrectes. | Una **factura rectificativa** (R1–R5). Un registre acceptat és immutable — l'esmena respon `record_not_rejected`. | | La correcció canvia un **camp de la huella**: NIF de l'emissor, sèrie + número, data d'expedició, tipus de factura, quota total o import total. | **Anul·lació + registre nou** (factura nova o rectificativa). L'esmena respon `requires_annulment` sense modificar res. | ## El flux [#flow] **Detecta el rebuig.** Subscriu-te a l'[esdeveniment de webhook](/guides/webhooks) `invoice.verifactu_failed`, o consulta el [llistat de registres](/api-reference/verifactu/public-api.v1.verifactu.records.list) cercant `status: rejected`. El registre porta el detall del rebuig de l'AEAT. **Corregeix les dades al seu origen.** El contingut reenviat es regenera des de la factura origen i les dades mestres actuals — p. ex. corregeix el NIF o la raó social del client i els valors nous es recullen automàticament. El snapshot legal congelat de la factura es refresca deliberadament perquè el PDF coincideixi amb el que rep l'AEAT. **Crida l'endpoint.** El registre torna a entrar a la cua de transmissió amb una ronda d'intents nova: ```bash curl -X POST https://api.factuarea.com/v1/verifactu/records/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/subsanar \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Resposta (`202`): ```json { "data": { "id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "message": "Subsanación encolada. El registro se reenviará a la AEAT en unos segundos." } } ``` **Vigila el resultat.** El registre es transmet de nou i acaba en `accepted` — o en `rejected` un altre cop si les dades continuen malament, i en aquest cas pots tornar a esmenar (no hi ha límit d'intents). ## Errors [#errors] Les violacions de regla de negoci retornen `422` amb `code: business_rule_violation` i un `subcode` que concreta la causa: ```json { "error": { "type": "invalid_request_error", "code": "business_rule_violation", "subcode": "record_not_rejected", "message": "El registro #842 no está rechazado por la AEAT (estado actual: accepted). Solo los registros rechazados admiten subsanación; para fallos técnicos usa el reintento." } } ``` | HTTP | `code` / `subcode` | Quan | | ---- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | 404 | `resource_not_found` | El registre no existeix o pertany a una altra empresa. | | 422 | `business_rule_violation` / `record_not_rejected` | El registre no està en `rejected` (està acceptat, pendent, enviat — o en `error` tècnic, que ja cobreix el reintent automàtic). | | 422 | `business_rule_violation` / `requires_annulment` | La correcció toca camps de la huella. Anul·la el registre i emet una factura nova o una rectificativa. | | 422 | `business_rule_violation` / `record_not_subsanable` | El registre no és d'*alta*, o no té factura origen des de la qual regenerar. | | 403 | `insufficient_scope` | La key no té el scope `verifactu:write`. | ## Evita el rebuig abans que passi [#prevention] El rebuig de dades més freqüent és un destinatari que el cens de l'AEAT no identifica (rebuig **1239**). Verifica el **parell nom + NIF** d'un client amb [`POST /v1/clients/census-verification`](/guides/census-verification#clients) **abans** d'emetre-li factures VeriFactu — així l'esmena es queda en el que ha de ser: una xarxa de seguretat, no una rutina. --- # Versionat (/ca/guides/versioning) L'API de Factuarea segueix una política d'URL amb **versionat pla** (`/v1`) combinada amb una capçalera de data opcional per a una evolució sense canvis incompatibles. El compromís és clar: un cop publicada, `/v1` es manté estable. Els canvis incompatibles requereixen `/v2`. ## Versió a l'URL [#versió-a-lurl] ``` https://api.factuarea.com/v1/... ``` `v1` és la nostra primera versió pública (maig de 2026). No hi ha versions anteriors accessibles. Quan es dissenyi `/v2`: * `/v1` i `/v2` coexisteixen durant **almenys 12 mesos**. * Les rutes de `/v1` no canvien en aquesta finestra (ni payloads, ni status codes, ni camps, ni semàntica). * Avisos per email als desenvolupadors amb keys actives, banner a la documentació, capçaleres a les respostes (vegeu més avall). ## Capçalera `Factuarea-Version` [#capçalera-factuarea-version] ```http Factuarea-Version: 2026-06-01 ``` El date-versioning funciona a l'estil de Stripe. Hi ha un **registre de versions suportades** (dates `YYYY-MM-DD`); avui n'hi ha una de sola, `2026-06-01`, que és també l'última. La versió que aplica a una petició — la **versió efectiva** — es resol en aquest ordre: 1. La capçalera de petició `Factuarea-Version`, si l'envies. 2. Si no, la versió **fixada a la teva API key** (s'estableix en crear la key; null vol dir "sempre l'última"). 3. Si no, l'**última** versió del registre. La versió efectiva es **retorna a cada resposta** a la capçalera `Factuarea-Version`, així sempre saps quina versió per data ha servit la teva petició. ```http Factuarea-Version: 2026-06-01 ``` Fixar una versió (per capçalera o a la key) congela el comportament del subconjunt d'endpoints que reben millores incrementals sense canvis incompatibles (nous camps a la resposta, nous paràmetres opcionals). Sense capçalera ni fixació, obtens l'última versió. El registre té avui dues dates: **`2026-06-01`** (la predeterminada, i la que obtens sense capçalera ni fixació) i **`2026-09-01`**. Optar per la més recent canvia dues coses, i cap més: | Canvi a `2026-09-01` | Què obtens a `2026-06-01` | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Les respostes d'esborrat massiu fan servir la forma transversal d'èxit parcial `{total, successful, failed, failures[{id, error_code, error_message}]}`. | La forma anterior `{object: "bulk_delete_result", deleted, failed[{id, reason}]}`. | | Els cinc errors preexistents dels gates de cobrament (`payment_method_required`, `seat_charge_failed`, `gestoria_plan_required`, `employee_seat_payment_method_required`, `employee_seat_charge_failed`) porten `error.type: "payment_required_error"`. | `error.type: "invalid_request_error"` en aquests cinc, exactament com abans. | La reclassificació de l'error **no** toca `error.code`, `error.subcode` ni l'estat HTTP — són `402` amb el mateix codi a totes les versions. Si ramifiques per `code` (que és el que recomanem), no canvia res per a tu en cap cas. `addon_required` és un codi posterior i porta sempre `payment_required_error`. ### Errors [#errors] La capçalera es valida contra el registre: * Valor **mal format** (que no sigui `YYYY-MM-DD`, p. ex. `2026-05` o `15/05/2026`) → `400 parameter_invalid_format` amb `param: "Factuarea-Version"`. * **Ben format però no suportat** (una data vàlida que no és al registre) → `400 unsupported_api_version` amb `param: "Factuarea-Version"`. Ometre la capçalera mai és un error — cau a la fixació de la key o a l'última versió. ## Què és un canvi incompatible? [#què-és-un-canvi-incompatible] Considerem **incompatible** (prohibit a `/v1`): * Reanomenar / eliminar camps de la resposta JSON. * Canviar el tipus d'un camp (`string` → `int`). * Canviar el `type`/`code` d'un embolcall d'error existent. * Canviar status codes (p. ex. retornar `201` on abans era `200`). * Convertir en obligatori un camp de la petició que abans era opcional. * Canviar el format d'un identificador (UUID v7 segueix sent UUID v7). * Eliminar un endpoint sense un reemplaçament documentat i una finestra de migració. * Canviar la semàntica de la màquina d'estats dels documents. Considerem **no incompatible** (permès sense una nova versió): * Afegir camps nous a les respostes. * Afegir paràmetres opcionals a les peticions. * Afegir endpoints nous. * Relaxar restriccions (apujar un límit, acceptar més formats). * Afegir valors d'enum nous **a camps que no siguin crítics per a les màquines d'estats del costat del client**. * Millorar els missatges d'error (canvia `message`, no `type`/`code`). * Reclassificar el `type` d'un envelope d'error existent **darrere d'una versió per data**: les keys fixades a una data anterior continuen rebent el `type` previ tal qual, i `code`/`subcode`/estat no es mouen. Així es van recategoritzar els cinc errors de cobrament a `2026-09-01`. Fer-ho *sense* versió per data és el cas incompatible que apareix a dalt. ## Política de deprecació [#política-de-deprecació] Quan un endpoint o camp es marca com a deprecat dins de `/v1` (p. ex. un àlies heretat reemplaçat per una versió canònica): * Avís per email als desenvolupadors amb keys actives afectades. * Banner a `docs.factuarea.com` amb el changelog. * Capçaleres a cada resposta de l'endpoint deprecat durant **almenys 12 mesos** abans de la seva retirada (que només passa a `/v2`): ```http Deprecation: true Sunset: Wed, 15 May 2027 00:00:00 GMT Link: ; rel="deprecation" Link: ; rel="alternate" ``` * A `/v1` l'endpoint **segueix funcionant** fins al llançament de `/v2`. Les capçaleres avisen. * A `/v2` l'endpoint es retira / reemplaça. La finestra entre el primer avís i `/v2` és ≥ 12 mesos. ## Migració entre versions [#migració-entre-versions] Cada migració (`v1 → v2`) ve acompanyada de: * Una guia dedicada a `docs.factuarea.com/guides/migration-v1-v2`. * Mapatge camp a camp i endpoint a endpoint. * Recomanacions operatives (mantenir totes dues keys, escriptura dual durant la transició). * Webhooks: els esdeveniments antics conserven la seva forma; els esdeveniments nous viuen en la seva pròpia versió declarada al payload. ## Changelog [#changelog] Cada canvi de `/v1` (camp nou, deprecació, esdeveniment nou, correcció de validació) es publica a [Changelog](/changelog/launch) amb etiquetes: * `feature` — camp / endpoint / esdeveniment nou. * `fix` — correcció d'un bug. * `deprecation` — camp o endpoint marcat com a obsolet (encara actiu a `/v1`). * `breaking` — només apareix a `/v2`, mai dins de `/v1`. * `security` — correcció amb implicacions de seguretat. Llegeix-la primer. Subscriu-te al feed RSS a `https://docs.factuarea.com/changelog.rss` o segueix `@factuarea` a X per als anuncis. ## Compromís d'estabilitat [#compromís-destabilitat] Una integració construïda avui contra `/v1` seguirà funcionant a `/v1` durant **almenys 24 mesos** des d'avui, sense tocar el teu codi. Finestra de suport per a v1 → mínim 12 mesos després del llançament de v2. Aquesta és la garantia. Qualsevol excepció es comunicarà amb terminis generosos. --- # Webhooks (/ca/guides/webhooks) Els webhooks notifiquen al teu servidor quan passa un esdeveniment a Factuarea (factura pagada, pressupost acceptat, client creat, etc.) sense que hagis de fer polling. Cada esdeveniment s'entrega a la teva URL mitjançant un `POST` HTTPS signat. **No s'entreguen en mode de prova.** Els esdeveniments generats amb una clau `fact_test_` (sandbox) es registren però **mai s'entreguen** als teus endpoints externs. Per exercitar la verificació de signatura del teu receptor en sandbox, fes servir l'endpoint dedicat `POST /v1/webhook_endpoints/{id}/ping`, que *sí* que s'entrega. Consulta [Mode de prova i sandbox](/guides/test-mode). Per validar el teu handler real end-to-end, fes servir [`test_event`](#test-deliveries) en el seu lloc. ## Crear un endpoint [#crear-un-endpoint] ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.mycompany.com/factuarea/webhook", "description": "Sync with internal CRM", "enabled_events": [ "invoice.created", "invoice.paid", "quote.approved" ] }' ``` Resposta (el `secret` es retorna **només una vegada**): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "webhook_endpoint", "url": "https://app.mycompany.com/factuarea/webhook", "description": "Sync with internal CRM", "enabled_events": ["invoice.created", "invoice.paid", "quote.approved"], "status": "enabled", "secret": "whsec_01HKQS5N8VR7QXJ9K3T6BWPMZA9876543210ABCDEF", "created_at": "2026-05-15T10:23:18Z" } ``` Per subscriure't a **tots els esdeveniments**, passa `"enabled_events": ["*"]`. El catàleg complet és a [Esdeveniments](/guides/events). ## Signatura HMAC SHA256 [#signatura-hmac-sha256] Cada entrega inclou aquestes headers: ```http Factuarea-Signature: t=1747314060,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd Factuarea-Event-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d Factuarea-Event-Type: invoice.paid Factuarea-Delivery-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c Idempotency-Key: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d ``` * `t` — timestamp UNIX de l'entrega (segons). * `v1` — HMAC SHA256 de la cadena `{t}.{body}` amb el `secret` de l'endpoint, en hex. * `Idempotency-Key` — el header estàndard de la indústria, amb el **mateix valor** que `Factuarea-Event-Id` (l'UUID v7 estable de l'esdeveniment). Fes-lo servir per deduplicar reentregues amb el header que el teu stack potser ja entén. Consulta [Idempotència per la teva banda](#idempotency-on-your-side). **Fas servir un SDK oficial?** Salta't l'HMAC manual de sota — tant el [SDK de TypeScript com el de PHP](/sdks) inclouen un verificador de webhooks que fa per tu la comparació en temps constant, la tolerància del timestamp i la gestió del període de gràcia de rotació. Consulta [Verificar webhooks amb el SDK](/sdks#verifying-webhooks). La recepta manual de sota és per a qualsevol altre llenguatge. Per validar: 1. Extreu `t` i `v1` de `Factuarea-Signature`. 2. Calcula `signed_payload = t + "." + raw_body` (bytes crus del body, sense reformatar el JSON). 3. Calcula `expected = hmac_sha256(secret, signed_payload)` en hex. 4. Compara `v1 == expected` fent servir **comparació en temps constant**. 5. Comprova que `|now - t| <= 300` (tolerància de ±5 minuts contra atacs de replay). Durant el període de gràcia d'una rotació de secret la header porta **dos** valors `v1` — un per cada secret actiu (`t=...,v1=,v1=`). Accepta la petició si coincideix **qualsevol** dels `v1`. Consulta [Rotació de secret](#secret-rotation-dual-signing). ```php function verifyFactuareaSignature( string $payload, string $signatureHeader, string $secret, int $toleranceSeconds = 300, ): bool { $timestamp = null; $signatures = []; foreach (explode(',', $signatureHeader) as $kv) { [$k, $v] = explode('=', $kv, 2); if ($k === 't') { $timestamp = (int) $v; } elseif ($k === 'v1') { $signatures[] = $v; } } if ($timestamp === null || $signatures === []) { return false; } if (abs(time() - $timestamp) > $toleranceSeconds) { return false; } $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret); foreach ($signatures as $candidate) { if (hash_equals($expected, $candidate)) { return true; } } return false; } // In the webhook handler: $payload = file_get_contents('php://input'); $header = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; $secret = getenv('FACTUAREA_WEBHOOK_SECRET'); if (! verifyFactuareaSignature($payload, $header, $secret)) { http_response_code(401); exit; } $event = json_decode($payload, true); handleEvent($event); http_response_code(200); ``` ```javascript const crypto = require('crypto'); function verifySignature(payload, header, secret, tolerance = 300) { let timestamp = null; const signatures = []; for (const kv of header.split(',')) { const [k, v] = kv.split('='); if (k === 't') timestamp = Number(v); else if (k === 'v1') signatures.push(v); } if (timestamp === null || signatures.length === 0) return false; if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${payload}`) .digest('hex'); return signatures.some((candidate) => crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(candidate, 'hex') ) ); } // Express: app.post('/factuarea/webhook', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString('utf8'); if (!verifySignature(payload, req.header('Factuarea-Signature'), process.env.WHSEC)) { return res.status(401).end(); } const event = JSON.parse(payload); handleEvent(event); res.status(200).end(); }); ``` ```python import hmac, hashlib, time from flask import request, abort def verify(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool: timestamp = None signatures = [] for kv in header.split(','): k, v = kv.split('=', 1) if k == 't': timestamp = int(v) elif k == 'v1': signatures.append(v) if timestamp is None or not signatures: return False if abs(time.time() - timestamp) > tolerance: return False signed = f"{timestamp}.".encode() + payload expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, candidate) for candidate in signatures) @app.post('/factuarea/webhook') def webhook(): if not verify(request.get_data(), request.headers.get('Factuarea-Signature', ''), WHSEC): abort(401) event = request.get_json() handle_event(event) return '', 200 ``` ## Verificar amb el SDK oficial [#verificar-amb-el-sdk-oficial] Els [SDK de TypeScript i PHP](/sdks) embolcallen els cinc passos de dalt — comparació en temps constant, tolerància de ±5 minuts i període de gràcia de rotació — en una sola crida. Passa el **body cru de la petició**, la header `Factuarea-Signature` i el secret de l'endpoint: ```ts import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Express, with express.raw({ type: "application/json" }) on the route: app.post("/webhooks/factuarea", (req, res) => { try { const event = factuarea.webhooks.verify( req.body.toString("utf8"), req.headers[SIGNATURE_HEADER.toLowerCase()] as string, process.env.FACTUAREA_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { /* … */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` Una tolerància personalitzada (en segons) és el quart argument opcional: `factuarea.webhooks.verify(body, header, secret, { toleranceSeconds: 600 })`. ```php use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier; use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException; $verifier = new WebhookVerifier(); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; try { $event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET')); // $event is the decoded, authenticated payload if (($event['type'] ?? null) === 'invoice.paid') { /* … */ } http_response_code(200); } catch (WebhookSignatureException $e) { http_response_code(400); } ``` Tots dos verificadors accepten **les dues** signatures `v1` durant el període de gràcia d'una rotació de secret (consulta [Rotació de secret](#secret-rotation-dual-signing)), de manera que una rotació mai descarta una entrega. ## Reintents [#reintents] Si el teu endpoint respon amb un status que **no és `2xx`** o no respon dins del `timeout_seconds` de l'endpoint (per defecte 10 s), Factuarea reintenta amb back-off exponencial: | Intent | Espera després de l'anterior | | ------ | ---------------------------- | | 1 | immediat | | 2 | 1 minut | | 3 | 5 minuts | | 4 | 30 minuts | | 5 | 2 hores | | 6 | 12 hores | | 7 | 1 dia | | 8 | 3 dies | Després de l'intent final l'entrega passa a `failed_permanently` i deixa de reintentar-se. Roman visible a `GET /v1/webhook_endpoints/{id}/deliveries` durant 30 dies, i la pots reintentar manualment mitjançant `POST /v1/webhook_endpoints/{id}/deliveries/{delivery_id}/replay` o des del dashboard. ## Body de l'entrega [#body-de-lentrega] El body de l'entrega és el mateix [objecte d'esdeveniment](/guides/events): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "api_version": "2026-05-22", "livemode": true, "test": false, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } } } ``` El camp `data` conté una **referència lleugera** al recurs afectat — recupera'l des del seu propi endpoint per obtenir la representació completa. L'`api_version` és sempre present als esdeveniments entregats (`null` només per a esdeveniments antics emesos abans de segellar les versions). El camp `test` és sempre present: `true` només per a una **entrega de prova** disparada des del dashboard (vegeu a sota), `false` per a esdeveniments reals. És **ortogonal a `livemode`**: `test` indica si *aquesta entrega* és de prova, mentre que `livemode` reflecteix l'**entorn de la key** (producció vs sandbox). Una entrega de prova es pot emetre en qualsevol dels dos, així que `livemode: true, test: true` és vàlid. ## Resposta esperada [#resposta-esperada] * Status `200`, `201`, `202` o `204` → entrega `delivered`. * Qualsevol altre status → entrega `failed`, es programa el següent reintent. * El body és irrellevant. **No** el processem — només es desen `response_status` i `duration_ms` al log d'entregues. ## Replay des del dashboard [#replay-des-del-dashboard] `Developers > Webhooks > Deliveries` et permet reintentar manualment qualsevol entrega, fins i tot les `failed_permanently`. Un reintent manual reinicia el comptador i deixa una entrada al log d'auditoria. ## Entregues de prova [#test-deliveries] Dos endpoints et permeten exercitar el teu receptor sense esperar un esdeveniment real — resolen problemes diferents: * `POST /v1/webhook_endpoints/{id}/ping` envia un payload **sintètic** `webhook.ping`. Mai entra al log d'esdeveniments ni és un tipus d'esdeveniment real. Fes-lo servir per confirmar l'accessibilitat i la verificació de signatura (és l'única entrega que es dispara en **sandbox**). * `POST /v1/webhook_endpoints/{id}/test_event` dispara una **entrega de prova d'un tipus d'esdeveniment real del catàleg**, marcada amb `"test": true` a l'envelope. Registra un `Event` real (visible a `GET /v1/events`) i encua un `WebhookDelivery` signat i reintentat **exactament igual que una entrega de producció** — així valides el teu handler real end-to-end. ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints/{id}/test_event \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "invoice.paid" }' ``` `type` és opcional: omet-lo per fer servir el primer esdeveniment subscrit de l'endpoint. Si l'indiques, ha de ser un dels `enabled_events` de l'endpoint (si no, `422 event_not_subscribed`). L'entrega arriba **només a aquest endpoint**, mai als altres endpoints subscrits al mateix tipus. ## Rotació de secret (dual-signing) [#secret-rotation-dual-signing] ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints/{id}/rotate_secret \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Retorna el nou secret. Durant **24 hores** (l'instant `previous_secret_valid_until` de la resposta) tots dos secrets són vàlids: cada entrega es signa **dues vegades** a la mateixa header `Factuarea-Signature` — un `v1` per secret (`t=...,v1=,v1=`). Després de la finestra, el secret antic queda invalidat. Et permet desplegar el nou secret amb zero temps d'inactivitat: 1. Crida `/rotate_secret` → obtén el nou `secret`. 2. Desplega el nou secret al teu entorn. 3. El teu handler accepta qualsevol dels `v1` durant el període de gràcia (els helpers de verificació de dalt ja recorren tots els `v1`). 4. Després de la finestra, només el nou secret està en ús. ## Idempotència de la teva banda [#idempotency-on-your-side] Cada esdeveniment inclou un camp `id` (UUID v7, únic). Els reintents del mateix esdeveniment sempre porten el mateix `id` — i el header `Idempotency-Key` porta aquest mateix valor exacte, així que pots deduplicar des del header sense parsejar el body. Persisteix els ids que hagis processat (taula `webhook_events_processed`) i retorna `200` sense actuar si ja l'has processat. ```python event_id = event['id'] if db.exists('webhook_events_processed', id=event_id): return '', 200 process(event) db.insert('webhook_events_processed', id=event_id, processed_at=now()) return '', 200 ``` ## Llista d'accés d'IP (opcional) [#llista-daccés-dip-opcional] Si el teu endpoint corre darrere d'un firewall que filtra per IP, pots restringir les IPs d'origen mitjançant `ip_allowlist` en crear l'endpoint. Factuarea entrega des d'un pool d'IPs estables documentades al dashboard. **Valida la signatura HMAC, no la IP** — les IPs poden canviar amb 30 dies d'avís, les signatures no. ## Esdeveniments disponibles [#esdeveniments-disponibles] El catàleg complet el retorna `GET /v1/event-catalog` i està documentat a [Esdeveniments](/guides/events). Exemples clau: * `invoice.created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.annulled` * `quote.created`, `quote.approved`, `quote.rejected`, `quote.converted` * `proforma.accepted`, `proforma.converted_to_invoice` * `delivery_note.signed` * `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested` * `client.created`, `client.updated` --- # Horaris de treball (/ca/guides/work-schedules) Un **horari de treball** modela les hores que una empresa **espera** d'un empleat: quantes hores al dia i a quina hora comença la jornada. Alimenta dos càlculs aigües avall — les **hores esperades** que usen els saldos, i l'**hora planificada d'entrada** que usa la [presència](/guides/presence) per marcar arribades tard. Els horaris estan acotats per `work_schedules:read` / `work_schedules:write` sota `https://api.factuarea.com/v1`. ## L'horari setmanal [#schedule] Un **horari setmanal** porta un nom, un **patró setmanal** de set dies —cada dia una llista de franges `HH:MM–HH:MM` no solapades—, un **mode** i un estat (`active` / `archived`). Les hores setmanals esperades i l'hora planificada es **deriven** del patró. El **mode** fixa com es mesura el compliment: | Mode | Significat | | --------------- | -------------------------------------------------------------------------------------------------- | | `validated` | Les hores esperades es prenen com a treballades un cop validades — l'horari és la font de veritat. | | `real_clocking` | El compliment es mesura contra els fitxatges reals del ledger. | El mode per defecte és `validated`. | Operació | Endpoint | | -------------------- | ---------------------------------------------------------------- | | Llistar / detall | `GET /v1/work-schedules`, `GET /v1/work-schedules/{schedule}` | | Crear / actualitzar | `POST /v1/work-schedules`, `PATCH /v1/work-schedules/{schedule}` | | Arxivar / desarxivar | `POST /v1/work-schedules/{schedule}/archive`, `.../unarchive` | | Estadístiques | `GET /v1/work-schedules/stats` | ```bash curl -X POST https://api.factuarea.com/v1/work-schedules \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Jornada completa 9 a 17", "mode": "validated", "week_pattern": { "monday": [{ "start": "09:00", "end": "17:00" }], "tuesday": [{ "start": "09:00", "end": "17:00" }], "wednesday": [{ "start": "09:00", "end": "17:00" }], "thursday": [{ "start": "09:00", "end": "17:00" }], "friday": [{ "start": "09:00", "end": "17:00" }], "saturday": [], "sunday": [] } }' ``` Un dia amb llista buida és un dia de descans. Consulta els esquemes a la [Referència d'API](/api-reference/work-schedules/public-api.v1.work_schedules.create). ## Assignacions [#assignments] Un horari s'aplica a un empleat mitjançant una **assignació efectiu-datada**: un `effective_from` (inclusiu) i un `effective_to` (exclusiu) opcional. Assignar un horari nou a un empleat **tanca l'assignació oberta anterior**, així que un empleat té un horari efectiu en qualsevol data sense buits ni solapaments. | Operació | Endpoint | Efecte | | ------------------------------ | ----------------------------------------------- | --------------------------------------------------------------- | | Assignar | `POST /v1/work-schedules/{schedule}/assign` | Obre una assignació des d'`effective_from`, tancant l'anterior. | | Desassignar | `POST /v1/work-schedules/{schedule}/unassign` | Tanca l'assignació oberta de l'empleat a aquest horari. | | Llistar assignacions | `GET /v1/work-schedules/{schedule}/assignments` | Els empleats assignats actualment. | | Resoldre l'horari de l'empleat | `GET /v1/work-schedules/employee/{employee}` | L'horari efectiu d'un empleat en una data donada. | ```bash curl -X POST https://api.factuarea.com/v1/work-schedules/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/assign \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "effective_from": "2026-01-07" }' ``` `GET /v1/work-schedules/employee/{employee}` és el contracte que consumeixen els saldos i la presència: retorna l'horari vigent de l'empleat a la data demanada, del qual es llegeixen les hores esperades i l'hora planificada. Les assignacions són datades per rang, no un camp solt a l'empleat. Reassignar un horari mai reescriu l'historial — l'assignació anterior es tanca amb un `effective_to`, i la nova s'obre des del seu `effective_from`. ## Flux típic [#flow] 1. Crea un **horari setmanal** amb el seu patró setmanal i el seu mode. 2. **Assigna'l** als empleats des d'una data `effective_from`. 3. Aigües avall, l'horari alimenta les **hores esperades** dels saldos i l'**hora planificada** que usa la [presència](/guides/presence) per marcar arribades tard. 4. **Desassigna** o reassigna a mesura que canvien els contractes; **arxiva** els horaris que ja no facis servir. ## Pròxims passos [#next] * [Presència](/guides/presence) — com l'hora planificada activa la detecció d'arribades tard. * [Tancament mensual](/guides/monthly-time-close) — on s'informen les hores esperades enfront de les treballades. --- # Visió general del control horari (/ca/guides/workforce-overview) El **control horari** de Factuarea cobreix l'obligació legal de les empreses espanyoles segons el **RD-llei 8/2019** (art. 34.9 de l'Estatut dels Treballadors): portar un registre **objectiu, fiable i inalterable** de la jornada diària de cada empleat, conservar-lo **quatre anys** i posar-lo a disposició de la Inspecció de Treball (ITSS). El registre es recolza en un **ledger de només apèndix** segellat per una **cadena d'empremtes SHA-256 per empresa** — el mateix patró antimanipulació que Factuarea aplica a la facturació [VeriFactu](/guides/glossary). És, en resum, el VeriFactu del fitxatge: res no s'edita ni s'esborra mai, i qualsevol manipulació trenca la cadena. Cada operació viu sota `https://api.factuarea.com/v1` i comparteix el mateix [embolcall d'error](/guides/errors), [paginació per cursor](/guides/pagination) i [scopes](/guides/scopes-and-irreversibility) que la resta de l'API. Tota la superfície està gatejada pel **mòdul `control_horario`**; una empresa que no el tingui rep un `403` en aquestes rutes. ## L'empleat, un rol només-portal [#employee-role] Un **empleat** és la persona treballadora que fitxa, té horari, sol·licita absències i merita saldos de jornada. És un rol **només-portal**: els empleats gestionen les seves pròpies dades des del portal i **mai** computen contra el límit de places `users` del pla. Donar d'alta empleats es factura en canvi mitjançant un add-on per plaça dedicat — consulta [Facturació de places d'empleat](/guides/employee-seats). ## Els vuit dominis [#domains] El sistema es reparteix en vuit dominis d'API. Comença per la guia de la tasca que tinguis entre mans; cadascuna enllaça als seus endpoints a la Referència d'API. | Domini | Què fa | Guia | Scope | | -------------------------- | ------------------------------------------------------------ | ----------------------------------------------- | ---------------------------------------------- | | Empleats | La plantilla: crear, editar, donar de baixa, reactivar. | — | `employees:read` / `employees:write` | | Horaris | Hores setmanals esperades i assignacions efectiu-datades. | [Horaris](/guides/work-schedules) | `work_schedules:read` / `work_schedules:write` | | Fitxatges | Entrada/sortida, pauses, fitxatges retroactius, correccions. | [Fitxatges](/guides/time-clock) | `time_entries:read` / `time_entries:write` | | Tancaments mensuals | Congelar, segellar, informar i exportar el registre. | [Tancament mensual](/guides/monthly-time-close) | `time_entries:read` / `time_entries:write` | | Exportacions per a nòmines | Fitxer d'incidències per a A3, Sage o NominaSOL. | [Tancament mensual](/guides/monthly-time-close) | `payroll_exports:read` | | Absències | Tipus, polítiques, sol·licituds, saldos i calendari. | [Absències](/guides/absences) | `absences:read` / `absences:write` | | Presència | Qui treballa ara, a l'oficina o en remot. | [Presència](/guides/presence) | `presence:read` | | Festius | Calendari nacional, autonòmic i local per comunitat. | — | `holidays:read` | Dos dominis són de **només lectura** a l'API: **presència** i **festius** exposen únicament lectures (`presence:read`, `holidays:read`). Declarar la presencialitat oficina/remot i crear festius locals propis són tasques només-portal — no existeix l'scope `presence:write` ni `holidays:write`. ## Els empleats i la plantilla [#employees] L'empleat és l'entitat àncora de la qual depèn la resta del sistema. Cada empleat porta un nom, un email únic per empresa, un `tax_id` i un `job_title` opcionals, les hores setmanals contractades, una data d'alta i la comunitat autònoma (`ccaa`) que determina quins festius apliquen. La baixa és una **baixa soft**: l'empleat conserva el seu historial al ledger (la retenció de quatre anys prohibeix destruir-lo) i pot reactivar-se després. ```bash curl -X POST https://api.factuarea.com/v1/employees \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Ana Ruiz", "email": "ana.ruiz@acme.example", "employment_type": "full_time", "contract_hours": 40, "hire_date": "2026-01-07", "ccaa": "ES-MD" }' ``` Consulta els esquemes complets de l'empleat a la [Referència d'API](/api-reference/employees/public-api.v1.employees.list). ## Scopes i MCP [#scopes] Cada domini mapeja a un scope fi del catàleg tancat (`employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read`, `payroll_exports:read`), tots gatejats rere el mòdul `control_horario`. Revisa la llista completa a la [pàgina de scopes](/guides/scopes-and-irreversibility) i al [catàleg de scopes MCP](/mcp/scopes). Cada ruta v1 té la seva [tool MCP](/mcp/tools) mirall, així que un agent pot executar les mateixes operacions. El ledger de jornada són dades de compliment, aïllades per disseny: mai referencia clients, factures ni projectes. Respon a una sola pregunta — quantes hores va treballar cada empleat — i manté aquesta evidència intacta. ## Per on seguir [#next] * [Fitxatges](/guides/time-clock) — entrada/sortida, pauses i el flux de correccions. * [Tancament mensual](/guides/monthly-time-close) — congelar, segellar i exportar el registre. * [Absències](/guides/absences) — tipus, polítiques, sol·licituds, saldos i arrossegament. * [Horaris](/guides/work-schedules) — patrons setmanals i assignacions. * [Presència](/guides/presence) — el panell d'equip en viu i la vista diària oficina/remot. * [Facturació de places d'empleat](/guides/employee-seats) — l'add-on per plaça i el seu cicle. --- # Resum de MCP (/ca/mcp) El **servidor MCP de Factuarea** exposa l'API pública com a tools de [Model Context Protocol](https://modelcontextprotocol.io), de manera que els agents d'IA (Claude, ChatGPT, Cursor, la teva pròpia app LLM) poden llegir i operar sobre les teves dades de facturació a través d'un únic endpoint governat en lloc d'escriure a mà crides HTTP. Parla el transport **Streamable HTTP** i viu a: ``` https://mcp.factuarea.com ``` L'endpoint canònic és l'arrel del subdomini. La forma anterior amb path `https://mcp.factuarea.com/mcp` continua funcionant com a àlies de compatibilitat, així que les configuracions existents segueixen connectant. Cada tool es correspon amb el mateix contracte `https://api.factuarea.com/v1` documentat a la Referència de l'API: recursos idèntics, el mateix `id` opac (UUID v7), els mateixos errors normalitzats, el mateix aïllament multi-tenant per empresa. La capa MCP afegeix descobriment (`tools/list`), aplicació de **scope** per tool i un flux de consentiment per a apps de tercers. La configuració recomanada — dues comandes instal·len el plugin oficial `factuarea-mcp` i connecten Claude Code sobre OAuth. Configura Claude Desktop, l'MCP Inspector o qualsevol client MCP manualment, fent servir OAuth o una API key. El catàleg complet agrupat per domini, amb el scope que requereix cada tool. ## Què pot fer [#què-pot-fer] El servidor publica ** tools** en 27 dominis. Tot el que pots fer amb la REST API ho pots fer aquí, en el format natiu de tool-calling de l'agent: Factures, pressupostos, factures proforma, albarans, factures recurrents i factures de compra — crear, actualitzar, transicionar, enviar i generar PDFs. Clients, proveïdors, productes, sèries de numeració i tipus impositius. Registres, esdeveniments i certificats de VeriFactu (AEAT), enviaments a FACe (FacturaE), a més de webhooks i el catàleg d'esdeveniments. Empleats, horaris de treball, el registre de fitxatges, tancaments mensuals, absències, presència i festius — el registre del RD-ley 8/2019. ## Quan fer servir MCP, REST o SDKs [#quan-fer-servir-mcp-rest-o-sdks] El servidor MCP, la REST API i els SDKs oficials són tres portes d'entrada al **mateix** backend. Tria segons qui (o què) està cridant: | Estàs construint… | Fes servir | Per què | | ------------------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | Un **agent / assistent d'IA** que raona sobre les teves dades i hi actua | **Servidor MCP** | Les tools s'autodescriuen; el model les descobreix i les crida sense que cablegis cada endpoint. Els scopes i el consentiment s'apliquen a cada crida. | | Un **servei backend, cron job o integració** amb lògica fixa | **REST API** | Determinista, sense model al bucle, control total sobre les peticions i els reintents. | | Un **client tipat** a la teva app (TypeScript o PHP) | [**SDKs oficials**](/sdks) | `@factuarea/sdk` i `factuarea/factuarea-php` embolcallen la REST API amb tipus, reintents i helpers d'idempotència. | Les tres superfícies comparteixen els mateixos identificadors, embolcall d'error i scopes, així que pots combinar-les: prototipa un flux amb un agent sobre MCP i després endureix el camí crític com a integració REST o SDK. ## Dues maneres d'autenticar-se [#dues-maneres-dautenticar-se] El mateix endpoint `/mcp` accepta dos tipus de credencial, per a dues audiències diferents: | Canal | Credencial | Per a | Tools assolibles | | ------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **API key** | Bearer token `fact_live_…` / `fact_test_…` | **El titular del compte** automatitzant la seva pròpia empresa (com un PAT de GitHub) | Fins a **** — concedeixes els scopes que vulguis, inclòs `*` | | **OAuth 2.1** | Access token emès via consentiment | **Apps de tercers** actuant en nom d'un usuari | **** — un catàleg curat que exclou les escriptures de VeriFactu, l'esborrat RGPD, FacturaE (FACe), Pagaments i passarel·les, i les tools de gestoria i escriptura de compte | Consulta [Connectar un client](/mcp/connect#channel-policy) per a la política completa de canals, i [Scopes i permisos](/mcp/scopes) per al catàleg de scopes. ## Construeix primer en mode de prova [#construeix-primer-en-mode-de-prova] Igual que a la REST API, una clau `fact_test_` — o un consentiment OAuth amb l'entorn **Test** seleccionat — opera sobre una **empresa sandbox** aïllada amb els efectes externs desactivats (sense transmissió a AEAT, sense emails reals, sense webhooks sortints). Construeix i valida contra test, després canvia a producció. Consulta [Mode de prova](/mcp/connect#test-mode). El servidor MCP està **inclòs en tots els plans de Factuarea**, juntament amb la resta de l'API pública. Crea una API key des de [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) (o connecta't per OAuth) i comença a cridar tools. --- # Plugin de Claude Code (/ca/mcp/claude-code-plugin) El marketplace de Factuarea publica **dos** plugins de Claude Code, per a dues feines diferents. **`factuarea-mcp`** és la manera més ràpida de connectar [Claude Code](https://claude.com/claude-code) al servidor MCP de Factuarea: registra el servidor (`https://mcp.factuarea.com`) i inclou una skill que ensenya a Claude a fer servir bé les tools — scopes, paginació per cursor, l'embolcall d'error i el mode de prova — perquè no hagis de configurar res a mà. **`factuarea-api`** serveix l'altra audiència, qui escriu el codi de la integració, i expressament **no declara cap servidor MCP**. Aquesta és la manera **recomanada** de connectar Claude Code. Prefereixes configurar el servidor manualment (altres clients, entorns headless)? Consulta [Connectar un client](/mcp/connect). ## Instal·lació [#installació] **Afegeix el marketplace** Registra el catàleg de plugins de Factuarea. Executa això dins de Claude Code: ```text /plugin marketplace add factuarea/claude-plugins ``` **Instal·la el plugin que necessitis** ```text /plugin install factuarea-mcp@factuarea ``` Claude Code instal·la el plugin i registra el servidor MCP `factuarea`. També escriuràs codi d'integració? Afegeix-hi [`factuarea-api`](#integrator-skills): tots dos són complementaris. Per obtenir actualitzacions més endavant, executa `/plugin marketplace update factuarea`. ## Connecta el servidor [#connecta-el-servidor] El plugin declara el servidor **sense capçalera d'auth**, així que la ruta recomanada és OAuth — mai s'enganxa res secret en un fitxer de configuració. **Autentica't** ```text /mcp ``` Tria **factuarea** i selecciona **Authenticate**. El teu navegador obre la pantalla de consentiment de Factuarea. El [Dynamic Client Registration](/mcp/connect#oauth-21) i PKCE passen automàticament — no hi ha cap client id ni secret per enganxar. **Aprova** A la pantalla de consentiment selecciones l'**empresa**, l'**entorn** (producció o prova) i els **scopes** que concedeixes. Els scopes sensibles (eliminacions, `invoices:void`) estan marcats i no vénen premarcats. Claude Code emmagatzema el token i el refresca de manera transparent. **Fes-lo servir** Demana a Claude que treballi amb les teves dades de Factuarea — "llista les factures impagades d'aquest trimestre en mode de prova", "crea un esborrany de factura per a Acme S.L.", "comprova la cadena VeriFactu". La skill es carrega automàticament; també la pots invocar explícitament: ```text /factuarea-mcp:factuarea-mcp ``` ### Connectar amb una API key en lloc d'OAuth [#connectar-amb-una-api-key-en-lloc-doauth] Per a entorns headless, o quan ja tens una clau `fact_`, connecta amb una capçalera estàtica en lloc d'OAuth: ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com \ --header "Authorization: Bearer fact_live_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Fes servir una clau `fact_test_` per apuntar al [sandbox](/mcp/connect#test-mode) aïllat. La superfície de l'API és idèntica — només el prefix canvia l'entorn. Amb una capçalera de clau **no** necessites el flux OAuth; la clau autentica cada petició. ## Què inclou `factuarea-mcp` [#què-inclou-factuarea-mcp] La declaració del servidor `factuarea` (`https://mcp.factuarea.com`, transport HTTP), perquè Claude pugui cridar directament totes les tools de Factuarea. Una skill que dóna a Claude el context per fer servir bé les tools — la política de canal, els dominis de tools i els seus scopes, la identitat UUID v7, la paginació per cursor, l'embolcall d'error i el mode de prova. Una instal·lació a part i més lleugera per escriure la mateixa integració — cinc skills, i cap declaració de servidor MCP: ni OAuth ni tools carregades. La skill de guia coneix la **política de canal** (una API key arriba a les tools; OAuth fa servir les curades, sense concedir mai `verifactu:write`, els scopes de FacturaE, Pagaments ni gestoria/escriptura de compte, ni l'operació GDPR d'oblit de signatura a apps de tercers), com el pla/mòdul i els feature flags acoten encara més `tools/list`, i que els canvis d'estat són **tools discretes** (`mark_invoice_as_paid`, `void_invoice`, `accept_quote`…), no un genèric `change_status`. ## Construir la integració: el plugin `factuarea-api` [#integrator-skills] El plugin anterior serveix per **operar el teu compte** mitjançant tools MCP. Un segon plugin cobreix la feina contrària: **escriure el codi** que crida la REST API des del teu propi backend: ```text /plugin install factuarea-api@factuarea ``` **No declara cap servidor MCP**, i això és el que el fa barat de tenir instal·lat: ni consentiment OAuth ni superfície de tools carregada a la sessió. Les seves cinc skills es carreguen segons la tasca que tinguis entre mans i es basen en els SDKs oficials, en l'especificació viva i en aquesta documentació. | Skill | Es carrega quan la tasca és… | Què cobreix | | ------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`factuarea-api`** | Començar, o preguntar què admet l'API, com funciona l'autenticació o què diu la documentació | El punt d'entrada: deu regles d'or, les dues capçaleres d'autenticació admeses i el prefix que decideix l'entorn, cerca local en aquesta documentació amb [`factuarea docs`](/cli/usage), i receptes que enruten a les quatre skills següents | | **`factuarea-implement`** | Muntar el client i fer les primeres crides | Triar entre el SDK de [TypeScript](/sdks/typescript) i el de [PHP](/sdks/php), resoldre la clau des de l'entorn, l'embolcall `data`, la [paginació per cursor](/guides/pagination), l'[`Idempotency-Key`](/guides/idempotency) a les escriptures i començar al sandbox | | **`factuarea-webhooks`** | Escriure o arreglar l'endpoint que rep les entregues | Verificació HMAC de `Factuarea-Signature` sobre el cos cru, comparació en temps constant, deduplicació per `Factuarea-Event-Id`, un 2xx ràpid amb la feina pesada diferida, la finestra de gràcia de la rotació i les proves en local amb `factuarea listen` | | **`factuarea-audit`** | Revisar una integració que ja existeix | Sis famílies de regles — verificació de signatura, idempotència a les escriptures, exposició de l'API key, gestió d'errors per `code`, límits de peticions i cicle de vida del document — reportant cada troballa amb una severitat, un `fitxer:línia` i la correcció concreta | | **`factuarea-upgrade`** | Realinear després d'un canvi de contracte o de SDK | Desalineació entre el codi i l'especificació viva, la versió fixada del SDK davant l'última publicada, i un informe que separa els canvis que trenquen dels additius, en l'ordre en què cal aplicar-los | Els dos plugins són complementaris, no alternatives. `factuarea-mcp` llegeix i actua sobre les teves dades mitjançant tools; `factuarea-api` no crida mai l'API per tu — escriu i revisa el codi que sí que ho fa. Els equips que construeixen una integració solen instal·lar tots dos. També pots generar tu mateix un client a partir de l'[especificació OpenAPI](/api/openapi). ## Resolució de problemes [#resolució-de-problemes] | Símptoma | Causa | Solució | | ------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Una tool retorna `401`** | No estàs autenticat, o la clau/token ha caducat. | Executa `/mcp` → **factuarea** → **Authenticate** per (re)iniciar OAuth, o revisa la teva capçalera d'API key. | | **`insufficient_scope` (`403`)** | La credencial no té el scope que requereix la tool. | Torna a autenticar-te i aprova el scope, o fes servir una clau que el tingui. Recorda que `verifactu:write` i la tool d'oblit de signatura són **només per a API key**. | | **Una tool que esperaves no apareix** | `tools/list` es filtra segons els teus scopes i feature flags. | Concedeix el scope (o fes servir una clau més àmplia); confirma que el canal de la credencial pot arribar-hi (OAuth exclou les tools de només API key). Això és l'esperat, no un bug. | | **`addon_not_active` (`-32007`)** | L'empresa no té un pla de Factuarea actiu que inclogui accés a l'API (p. ex. un trial caducat). | Contracta o renova un pla des del dashboard; tota la superfície MCP requereix un pla actiu. | | **`429` amb `Retry-After`** | S'ha arribat a un bucket de [límit de peticions](/mcp/errors#rate-limits). | Espera els segons de `Retry-After` abans de reintentar — no insisteixis sense parar. | Consulta [Errors i límits de peticions](/mcp/errors) per a la taula completa de codis. --- # Connectar un client (/ca/mcp/connect) El servidor MCP de Factuarea parla **Streamable HTTP** a `https://mcp.factuarea.com`. Qualsevol client compatible amb MCP es pot connectar fent servir una de les dues credencials admeses: * **OAuth 2.1** — el client es registra a si mateix i l'usuari l'autoritza mitjançant una pantalla de consentiment. Ideal per a eines d'usuari final. * **API key** — passes un Bearer token `fact_live_` / `fact_test_` directament. Ideal per a les teves pròpies automatitzacions. L'endpoint canònic és l'arrel del subdomini, `https://mcp.factuarea.com`. La forma anterior amb ruta `https://mcp.factuarea.com/mcp` continua funcionant com a àlies de compatibilitat. **Fas servir Claude Code?** La configuració recomanada és el plugin oficial `factuarea-mcp` — dues comandes i estàs connectat per OAuth, amb una skill d'orientació inclosa. Consulta la guia dedicada del [plugin de Claude Code](/mcp/claude-code-plugin). Els passos manuals de sota són per a altres clients o configuracions headless. ## Claude Code [#claude-code] El camí més senzill és el [plugin de Claude Code](/mcp/claude-code-plugin) — registra el servidor i inclou una skill d'orientació en una sola instal·lació. Si prefereixes connectar el servidor a mà, [Claude Code](https://claude.com/claude-code) també admet servidors MCP remots sobre HTTP amb OAuth integrat. ### Amb OAuth [#amb-oauth] **Afegeix el servidor** ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com ``` **Autentica't** Dins de Claude Code, executa el slash command: ```text /mcp ``` Tria **factuarea**, selecciona **Authenticate** i el teu navegador obre la pantalla de consentiment. Selecciona l'**empresa** a la qual donar accés, l'**entorn** (live o test) i els **scopes** que vulguis permetre, i després confirma. Claude Code emmagatzema el token resultant i el renova automàticament. **Fes-lo servir** Demana a Claude que faci alguna cosa — "llista les meves factures vençudes en mode de prova" — i descobreix i crida les tools corresponents. ### Amb una API key [#amb-una-api-key] Si prefereixes fer servir la teva pròpia clau (sense flux de consentiment), passa-la com un header `Authorization`: ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com \ --header "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Els scopes de la clau determinen quines tools apareixen a `tools/list`. Una clau amb `*` veu les tools; una clau més restringida només veu les tools que cobreixen els seus scopes. ## Claude Desktop [#claude-desktop] [Claude Desktop](https://claude.com/download) es connecta a servidors remots mitjançant el seu fitxer de configuració. Afegeix una entrada sota `mcpServers`: ```json { "mcpServers": { "factuarea": { "type": "http", "url": "https://mcp.factuarea.com" } } } ``` En el següent arrencament, Claude Desktop descobreix el servidor i et guia pel flux de consentiment OAuth al teu navegador. Per fer servir una API key en lloc d'això, afegeix un objecte `headers`: ```json { "mcpServers": { "factuarea": { "type": "http", "url": "https://mcp.factuarea.com", "headers": { "Authorization": "Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` El fitxer de configuració es troba a `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Reinicia l'app després d'editar-lo. ## MCP Inspector [#mcp-inspector] El [MCP Inspector](https://github.com/modelcontextprotocol/inspector) és la manera més ràpida d'explorar el catàleg i cridar tools a mà mentre desenvolupes. **Arrenca'l** ```bash npx @modelcontextprotocol/inspector ``` **Connecta** Configura **Transport** a `Streamable HTTP` i **URL** a `https://mcp.factuarea.com`. Per a OAuth, l'Inspector executa el flux d'autorització per tu. Per a una API key, afegeix un header `Authorization: Bearer fact_test_…` sota **Authentication**. **Explora** Obre **Tools → List Tools** per veure totes les tools que la teva credencial pot abastar, inspecciona el seu esquema d'entrada i executa-la amb arguments d'exemple. ## Qualsevol client MCP [#qualsevol-client-mcp] El servidor segueix l'especificació MCP, així que qualsevol client conforme funciona. L'essencial: * **Endpoint** — `https://mcp.factuarea.com` (la forma amb ruta `…/mcp` és un àlies de compatibilitat) * **Transport** — Streamable HTTP * **Auth** — `Authorization: Bearer `, on el token és una API key (`fact_live_` / `fact_test_`) o un access token d'OAuth 2.1 * **Discovery** — davant d'un `401`, el servidor retorna un header `WWW-Authenticate` que apunta a les seves [Protected Resource Metadata](#discovery) (RFC 9728) perquè els clients trobin l'authorization server automàticament El descobriment de tools està paginat; el servidor retorna el catàleg complet (fins a 200 tools) en una sola resposta `tools/list` per defecte, i respecta `nextCursor` si el teu client pagina. ## Autenticació [#authenticate] El servidor MCP accepta dos tipus de credencial al mateix endpoint `https://mcp.factuarea.com`, per a dues audiències diferents. Totes dues arriben com a `Authorization: Bearer `; el servidor les distingeix per la forma del token (`fact_*` → API key, qualsevol altra cosa → access token d'OAuth). ### Política de canal [#channel-policy] Aquesta és la regla més important de la superfície MCP: | Canal | Qui | Com es trien els scopes | Tools accessibles | | ------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------- | | **API key** | El **propietari del compte** automatitzant la seva pròpia empresa | Tries els scopes en crear la clau — fins al super-scope `*` | **** (tot) | | **OAuth 2.1** | Una **app de tercers** actuant en nom d'un usuari | L'usuari concedeix els scopes a la pantalla de consentiment, des d'un **catàleg curat** | **** | El model reflecteix el de GitHub: un **personal access token** (API key) és la credencial pròpia del propietari i pot tenir qualsevol permís, mentre que una **OAuth app** és externa i es limita a un conjunt verificat de scopes que l'usuari aprova explícitament. Les tools que una OAuth app **mai** pot abastar (només una API key pot) són les operacions fiscals i de privacitat més sensibles, a més de la superfície first-party de gestió del compte: * **Escriptures de VeriFactu** (`verifactu:write`) — 8 tools: registrar/reintentar/subsanar registres i esdeveniments de VeriFactu, pujar/activar/revocar certificats FNMT, actualitzar ajustos de VeriFactu. Toquen el compliment de l'AEAT i són exclusives del propietari. * **Esborrament RGPD** (`delivery_notes:gdpr_forget`) — 1 tool: esborrar la PII d'auditoria de signatura (Art. 17). Privilegiada, només per a administradors. * **FacturaE (FACe)** (`facturae:read` / `facturae:write`) — 5 tools: les operacions B2G de FACe. Els seus scopes encara no són al catàleg de consentiment OAuth, així que per ara són només per a API key. * **Pagaments i passarel·les** (`stripe_autoinvoicing:*`, `payouts:read`) — 10 tools: configuració d'auto-facturació de Stripe Connect, comptes connectats i payouts. Scopes granulars, només API key, sense equivalent de consentiment OAuth. * **Gestoria** (`companies:*`, `api_keys:*`) — 16 tools: gestió de les empreses filles i les seves API keys. Gestionar sub-comptes i credencials és first-party, mai es concedeix per consentiment de tercers. * **Escriptures de compte** (`account:write`) — 4 tools: crear/rotar/revocar les teves pròpies API keys i actualitzar la personalització del compte. Només first-party. Tota la resta — les tools de lectura/escriptura/transició/enviament — està disponible per a tots dos canals. Consulta [Scopes & permisos](/mcp/scopes) per veure el catàleg. ### API keys [#api-keys] Una API key és un Bearer token opac vinculat a la teva empresa, creat al dashboard de desenvolupadors a [Ajustos → Desenvolupadors → API Keys](https://app.factuarea.com/settings/developers/api-keys). El format i les regles són idèntics als de l'API REST: ``` fact_live_<24 alphanumeric characters> → production company fact_test_<24 alphanumeric characters> → isolated sandbox company ``` El prefix és la font de veritat de l'**entorn** — consulta [Mode de prova](#test-mode). El secret es mostra **només una vegada** en la creació; el backend només emmagatzema un hash bcrypt. Passa'l al teu client MCP com a: ``` Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` Per al cicle de vida complet de la clau — creació, scopes, rotació amb període de gràcia, revocació, llista d'accés d'IPs, `expires_at` — consulta la [guia d'Autenticació](/guides/authentication) canònica. Les claus es comparteixen entre les superfícies REST i MCP. ### OAuth 2.1 [#oauth-21] Per a apps de tercers, Factuarea és un **OAuth 2.1 Authorization Server** complet. És compatible amb Dynamic Client Registration, el flux d'authorization-code amb PKCE i la rotació de refresh-token. No cal pre-registre ni aprovació manual de l'app — un client es registra a si mateix i l'usuari l'autoritza. (Els clients interactius com Claude Code i el MCP Inspector executen tot aquest flux per tu; els passos de sota són per construir el teu propi client.) #### Descobriment [#discovery] Els clients descobreixen les capacitats del servidor a través d'endpoints de metadata estàndard (sense prefix `/api`): | Endpoint | RFC | Propòsit | | ----------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `/.well-known/oauth-authorization-server` | [8414](https://www.rfc-editor.org/rfc/rfc8414) | Authorization Server Metadata — llista els endpoints authorize/token/register/introspect/revoke, els scopes admesos, `code_challenge_methods_supported: ["S256"]`. | | `/.well-known/oauth-protected-resource` | [9728](https://www.rfc-editor.org/rfc/rfc9728) | Protected Resource Metadata — declara el recurs MCP i quin authorization server emet tokens vàlids. | Quan una petició no autenticada abasta l'endpoint, el servidor respon `401` amb un header `WWW-Authenticate: Bearer ..., resource_metadata=""` perquè els clients RFC 9728 trobin l'authorization server sense endevinar. #### 1. Dynamic Client Registration (RFC 7591) [#1-dynamic-client-registration-rfc-7591] Un client es registra a si mateix fent POST de la seva metadata; el servidor retorna un `client_id` (i un `client_secret` per a clients confidencials): ```bash curl -X POST https://mcp.factuarea.com/api/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "My Invoicing Assistant", "redirect_uris": ["https://myapp.example.com/callback"], "token_endpoint_auth_method": "none" }' ``` Els clients públics (apps de navegador/natives) es registren amb `token_endpoint_auth_method: "none"` i depenen de PKCE; els clients confidencials fan servir `client_secret_basic`. El registre té un rate limit de **60 per minut per IP**. #### 2. Autorització amb PKCE [#2-autorització-amb-pkce] Envia l'usuari a l'endpoint authorize amb un challenge PKCE (`code_challenge_method=S256` és l'únic mètode acceptat): ``` GET https://mcp.factuarea.com/api/oauth/authorize ?response_type=code &client_id= &redirect_uri=https://myapp.example.com/callback &scope=factuarea.read invoices.write &state= &code_challenge= &code_challenge_method=S256 ``` Això renderitza la **pantalla de consentiment**, on l'usuari: 1. Tria l'**empresa** a la qual donar accés (un usuari pot pertànyer a diverses). 2. Tria l'**entorn** — **live** (l'empresa real) o **test** (un sandbox aïllat), a l'estil Stripe. Test és opt-in; si és absent ⇒ live. 3. Revisa i **selecciona els scopes** a concedir. Els scopes sensibles es marquen i no vénen pre-seleccionats. En aprovar, el servidor redirigeix de tornada amb un `code` d'un sol ús (i el teu `state`). Les operacions sensibles es filtren del catàleg que l'usuari pot aprovar — consulta la [política de canal](#channel-policy). #### 3. Intercanvi de token [#3-intercanvi-de-token] Intercanvia el code per un access token, enviant el verificador PKCE: ```bash curl -X POST https://mcp.factuarea.com/api/oauth/token \ -d grant_type=authorization_code \ -d code= \ -d redirect_uri=https://myapp.example.com/callback \ -d client_id= \ -d code_verifier= ``` ```json { "access_token": "", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "", "scope": "profile.read clients.read invoices.read invoices.write" } ``` El token persisteix els scopes **expandits i de gra fi** (les macros com `factuarea.read` s'expandeixen en el moment de l'emissió). Fes servir l'access token com a credencial Bearer. L'endpoint token té un rate limit de **60 per minut per (client, IP)** i requereix autenticació de client (HTTP Basic per a clients confidencials, `client_id` al body per als públics). #### 4. Rotació de refresh-token [#4-rotació-de-refresh-token] Els refresh tokens **roten per família**: cada refresh emet un nou access token i un nou refresh token, i invalida el que vas fer servir. ```bash curl -X POST https://mcp.factuarea.com/api/oauth/token \ -d grant_type=refresh_token \ -d refresh_token= \ -d client_id= ``` Si un refresh token es **reutilitza** (usat després de la rotació — el senyal clàssic d'una fuita), el servidor detecta la reutilització, revoca tota la família de tokens i llança una alerta de seguretat. Emmagatzema i fes servir sempre només el darrer refresh token. #### Revocació i introspecció [#revocació-i-introspecció] | Endpoint | RFC | Propòsit | | ---------------------------- | ---------------------------------------------- | ------------------------------------------------------------------- | | `POST /api/oauth/revoke` | [7009](https://www.rfc-editor.org/rfc/rfc7009) | Revoca un access o refresh token. | | `POST /api/oauth/introspect` | [7662](https://www.rfc-editor.org/rfc/rfc7662) | Comprova si un token està actiu i llegeix els seus scopes/metadata. | Tots dos requereixen autenticació de client. Els usuaris també poden revisar i revocar apps connectades des del dashboard de Factuarea, i a un administrador d'empresa que perd l'accés se li revoquen els tokens automàticament en la següent crida. ## Mode de prova [#test-mode] El servidor MCP funciona contra els mateixos dos **entorns** que l'API REST — **live** (la teva empresa real) i **test** (un sandbox aïllat) — perquè puguis construir i validar una integració d'agent sense tocar dades de producció, l' AEAT ni les safates d'entrada dels teus clients. Com selecciones el mode de prova depèn del canal: | Canal | Com fer servir el mode de prova | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **API key** | Autentica't amb una clau `fact_test_`. El prefix és la font de veritat — un token `fact_test_` sempre opera sobre el sandbox. | | **OAuth 2.1** | A la pantalla de consentiment, tria l'entorn **Test** (a l'estil Stripe). Si és absent ⇒ **live**. El token emès queda vinculat a aquest entorn. | Una credencial de test opera sobre una **empresa sandbox** dedicada — un bessó tècnic de la teva empresa real, aprovisionat automàticament i que hereta el seu pla, perquè el gating de mòdul/pla es comporti fidelment. L'aïllament és **estructural** (les dades de test i live viuen en empreses separades), i els efectes externs estan desactivats: | Efecte | A `live` | A `test` | | ------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | **VeriFactu** | El registre d'Alta es crea i es transmet a l'AEAT. | Es crea **localment**, però **mai es transmet** a l'AEAT. | | **Email** | Els emails de documents arriben als destinataris reals. | **No s'entreguen** als destinataris reals. | | **Webhooks** | Els esdeveniments subscrits s'entreguen als teus endpoints. | Es registren amb `livemode: false`, però **no s'entreguen**. | | **FACe (FacturaE)** | Els enviaments es presenten al web service real de FACe. | **Simulats** — cap crida SOAP surt de Factuarea; el número de registre és sintètic (`FACE-SANDBOX-*`). | Tota la resta es comporta exactament igual que en producció, i el conjunt complet de tools està disponible en tots dos entorns (subjecte als teus scopes i pla). Quan el teu flux funcioni d'extrem a extrem, canvia a live: crea una clau `fact_live_`, o torna a executar el flux de consentiment i selecciona l'entorn **live**. Aquest és el mateix mecanisme de sandbox que l'API REST. Consulta la guia canònica [Mode de prova & sandbox](/guides/test-mode) per saber com s'aprovisiona i es consulta l'empresa sandbox. --- # Errors i límits de peticions (/ca/mcp/errors) El servidor MCP parla **JSON-RPC 2.0** estricte. Els errors tornen com un objecte `error`, mai com un cos d'error HTTP a l'estil de l'API REST — però la **semàntica és idèntica**: la mateixa violació de regla de negoci que retorna `422` sobre REST retorna aquí l'error JSON-RPC equivalent, amb el `code` i l'`http_status` de v1 conservats a `data`. Aquesta pàgina cobreix el mapatge JSON-RPC específic d'MCP i els buckets de throttling d'MCP. Per al contracte REST canònic — l'embolcall d'error per `code` i les quotes per tier — consulta [Errors](/guides/errors) i [Límits de peticions](/guides/rate-limits). ## Forma de l'error [#forma-de-lerror] ```json { "jsonrpc": "2.0", "id": "", "error": { "code": -32008, "message": "invoice_cannot_be_modified", "data": { "http_status": 422, "code": "invoice_cannot_be_modified", "hint": "La factura ya emitida no puede modificarse.", "param": "status" } } } ``` * **`error.code`** — el codi numèric JSON-RPC (sempre dins del rang `-32099..-32000` definit per la implementació, o `-32603` per a errors interns). * **`error.message`** — un identificador de cadena estable (p. ex. `insufficient_scope`, `invoice_cannot_be_modified`). * **`error.data.code`** — el mateix `code` canònic de v1 que retorna l'API REST, de manera que puguis ramificar segons un únic valor a totes dues superfícies. * **`error.data.http_status`** — l'estat HTTP que retornaria la crida REST equivalent (422 / 404 / 409 / …), per a clients que prefereixen raonar en termes HTTP. * **`error.data.hint`** — un missatge llegible per a persones (en castellà, d'acord amb l'idioma de l'app). Altres camps (`param`, `subcode`, `required_scope`, …) apareixen quan són rellevants. ## Taula de codis [#taula-de-codis] | JSON-RPC code | `message` / `data.code` | HTTP equiv. | Significat | | ------------- | --------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-32001` | `invalid_token` | 401 | Credencial absent, malformada o desconeguda; o l'usuari ja no és membre de l'empresa. | | `-32002` | `client_revoked` | 403 | El client OAuth va ser revocat. | | `-32003` | `invalid_token_type` | 403 | Tipus de credencial incorrecte per a aquesta superfície. | | `-32004` | `plan_limit_exceeded` / `plan_upgrade_required` | 402 | S'ha assolit un límit d'ús del pla, o l'acció requereix un pla superior. `data` inclou `resource`, `current`, `limit`. | | `-32005` | `insufficient_scope` / `module_not_in_plan` / `feature_flag_disabled` | 403 | La credencial no té el scope requerit, el mòdul no és al pla, o un feature flag està desactivat. `data` inclou `required_scope` / `module` / `flag`. | | `-32006` | `rate_limit_exceeded` | 429 | S'ha superat un bucket de throttling. `data` inclou `retry_after` i `bucket`; la resposta també estableix la capçalera `Retry-After`. | | `-32007` | `addon_not_active` | 403 | L'empresa no té un pla de Factuarea actiu que inclogui accés a l'API pública (p. ex. un trial caducat o una subscripció vençuda fora del seu període de gràcia). | | `-32008` | *(codi v1)* | 422 / 404 / 409 / … | Una violació de regla de negoci, recurs inexistent o conflicte. `message` i `data.code` són el codi d'error canònic de v1; `data.http_status` t'indica la categoria. | | `-32603` | `internal_error` | 500 | Error inesperat del servidor. | `-32005` i `-32008` cobreixen cadascun diverses subcauses. Ramifica sempre segons `data.code` (la cadena), no només segons el `code` numèric, quan necessitis distingir-les — p. ex. `insufficient_scope` i `module_not_in_plan` afloren tots dos com a `-32005`. ### Scope insuficient [#scope-insuficient] Quan una tool necessita un scope que la credencial no té: ```json { "jsonrpc": "2.0", "id": "req-42", "error": { "code": -32005, "message": "insufficient_scope", "data": { "http_status": 403, "code": "insufficient_scope", "required_scope": "invoices:write", "provided_scopes": ["invoices:read", "clients:read"], "hint": "La credencial no tiene el scope requerido para esta operación." } } } ``` Les tools que la teva credencial no pot assolir també queden **ocultes** a `tools/list`, de manera que un agent ben comportat normalment no les intentarà — aquest error és la xarxa de seguretat. ## Límits de peticions [#límits-de-peticions] Les peticions es limiten en tres buckets independents. Superar-ne qualsevol retorna `-32006` amb una capçalera `Retry-After` (segons). ### Per token, per categoria de tool [#per-token-per-categoria-de-tool] Cada credencial té comptadors per minut separats per **categoria** de tool, de manera que un ús destructiu intensiu no pugui esgotar les teves lectures: | Categoria | Límit per defecte | Tools d'exemple | | ---------------- | ----------------- | --------------------------------------- | | `read` / `write` | 60 / min | `search_invoices`, `create_invoice` | | `send` | 20 / min | `send_invoice`, `send_quote` | | `generate` | 30 / min | `get_invoice_facturae_link` | | `destructive` | 10 / min | `delete_invoice`, `bulk_delete_clients` | El bucket es resol a partir de la [categoria](/mcp/tools#how-to-read-this-catalog) de la tool. El comptador s'incrementa **abans** que la tool s'executi, així que les crides rebutjades (scope incorrecte, error de validació) també consumeixen quota — això és anti-abús deliberat, d'acord amb el patró estàndard OAuth/REST. ### Per pla, cada hora [#per-pla-cada-hora] Un sostre horari a nivell d'empresa per slug de pla. El pla **Enterprise** se salta aquest bucket completament. ### Per client OAuth, global [#per-client-oauth-global] Les apps OAuth comparteixen a més un bucket global per client de **1000 / min**, de manera que una sola app que es comporti malament no pugui saturar el servidor a través de tots els seus usuaris. ### Capçaleres de límit de peticions [#capçaleres-de-límit-de-peticions] Les respostes correctes porten el pressupost restant perquè puguis afluixar el ritme de manera proactiva: | Header | Significat | | --------------------------------------------------------- | -------------------------------------------------- | | `X-RateLimit-Limit-Token` / `X-RateLimit-Remaining-Token` | El bucket per token (per categoria). | | `X-RateLimit-Limit-Hour` / `X-RateLimit-Remaining-Hour` | El bucket per pla horari (absent a Enterprise). | | `Retry-After` | En un `429`, segons a esperar abans de reintentar. | ### Límits dels endpoints d'auth [#límits-dels-endpoints-dauth] Els endpoints OAuth tenen els seus propis límits, independents dels buckets d'MCP: | Endpoint | Límit | | -------------------------- | ------------------------- | | `POST /api/oauth/register` | 10 / hora per IP | | `POST /api/oauth/token` | 60 / min per (client, IP) | Respecta sempre `Retry-After`. Reintentar abans que transcorri manté el bucket ple i només retarda la teva recuperació. Combina'l amb idempotència a les escriptures perquè un reintent retardat mai no dupliqui un document. --- # Scopes i permisos (/ca/mcp/scopes) Cada tool MCP declara el **scope** que una credencial ha de tenir per invocar-la. Els scopes funcionen de manera lleugerament diferent segons el canal: * Les **API keys** es creen directament amb scopes **detallats** (`resource:action`, p. ex. `invoices:read`) — el mateix catàleg tancat que fa servir l'API REST. També pots concedir el super-scope `*`. * Els **tokens OAuth** reben scopes amb punt (`resource.action`, p. ex. `invoices.read`) a la pantalla de consentiment. El servidor els tradueix als scopes detallats automàticament, de manera que tots dos canals apliquen el mateix conjunt al límit de la tool. ## Catàleg de consentiment OAuth [#catàleg-de-consentiment-oauth] Aquests són els scopes que un usuari pot concedir a una app de tercers a la pantalla de consentiment. Hi ha **59 scopes simples** més **3 macros**. ### Scopes simples [#scopes-simples] Cadascun concedeix una capacitat. La columna **Maps to** mostra el scope detallat que apliquen les tools — la capa de consentiment tradueix els scopes OAuth amb punt a aquests automàticament. La columna **Sensitive** marca els scopes que la pantalla de consentiment destaca i no marca per defecte. #### Perfil [#perfil] | Scope | Concedeix | Maps to | Sensitive | | -------------- | ------------------------------------------ | -------------- | --------- | | `profile.read` | Llegir el teu nom, email i empresa activa. | `account:read` | no | #### CRM — clients i proveïdors [#crm--clients-i-proveïdors] | Scope | Concedeix | Maps to | Sensitive | | ------------------ | ------------------------------- | ------------------ | --------- | | `clients.read` | Llistar i llegir clients. | `clients:read` | no | | `clients.write` | Crear i actualitzar clients. | `clients:write` | no | | `clients.delete` | Eliminar clients. | `clients:delete` | ⚠ sí | | `suppliers.read` | Llistar i llegir proveïdors. | `suppliers:read` | no | | `suppliers.write` | Crear i actualitzar proveïdors. | `suppliers:write` | no | | `suppliers.delete` | Eliminar proveïdors. | `suppliers:delete` | ⚠ sí | #### Catàleg — productes, sèries, impostos [#catàleg--productes-sèries-impostos] | Scope | Concedeix | Maps to | Sensitive | | ----------------- | ----------------------------------------- | ----------------- | --------- | | `products.read` | Llistar i llegir el catàleg de productes. | `products:read` | no | | `products.write` | Crear i actualitzar productes. | `products:write` | no | | `products.delete` | Eliminar productes. | `products:delete` | ⚠ sí | | `series.read` | Llegir sèries de numeració. | `series:read` | no | | `series.write` | Crear i actualitzar sèries de numeració. | `series:write` | no | | `taxes.read` | Llegir tipus impositius i retencions. | `taxes:read` | no | | `taxes.write` | Crear i actualitzar tipus impositius. | `taxes:write` | no | #### Vendes — factures, pressupostos, proformes, albarans [#vendes--factures-pressupostos-proformes-albarans] | Scope | Concedeix | Maps to | Sensitive | | ---------------------------- | ------------------------------------------------------- | --------------------------- | --------- | | `invoices.read` | Llistar i llegir factures. | `invoices:read` | no | | `invoices.write` | Crear i actualitzar factures. | `invoices:write` | no | | `invoices.send` | Enviar factures per email. | `invoices:send` | no | | `invoices.delete` | Eliminar factures en esborrany. | `invoices:delete` | ⚠ sí | | `invoices.annul` | Anul·lar factures emeses. | `invoices:void` | ⚠ sí | | `invoices.create_corrective` | Emetre factures rectificatives. | `invoices:write` | no | | `quotes.read` | Llistar i llegir pressupostos. | `quotes:read` | no | | `quotes.write` | Crear i actualitzar pressupostos. | `quotes:write` | no | | `quotes.send` | Enviar pressupostos per email. | `quotes:send` | no | | `quotes.delete` | Eliminar pressupostos. | `quotes:delete` | ⚠ sí | | `quotes.convert_to_invoice` | Acceptar/rebutjar i convertir pressupostos en factures. | `quotes:transition` | no | | `proformas.read` | Llistar i llegir factures proforma. | `proformas:read` | no | | `proformas.write` | Crear i actualitzar proformes. | `proformas:write` | no | | `proformas.send` | Enviar proformes per email. | `proformas:send` | no | | `proformas.delete` | Eliminar proformes. | `proformas:delete` | ⚠ sí | | `proformas.convert` | Convertir proformes en factures. | `proformas:transition` | no | | `delivery_notes.read` | Llistar i llegir albarans. | `delivery_notes:read` | no | | `delivery_notes.write` | Crear, actualitzar i enviar albarans. | `delivery_notes:write` | no | | `delivery_notes.send` | Enviar albarans per email. | `delivery_notes:write` | no | | `delivery_notes.delete` | Eliminar albarans. | `delivery_notes:delete` | ⚠ sí | | `delivery_notes.convert` | Convertir albarans. | `delivery_notes:transition` | no | | `delivery_notes.sign` | Marcar com a lliurats / signar albarans. | `delivery_notes:transition` | ⚠ sí | #### Compres [#compres] | Scope | Concedeix | Maps to | Sensitive | | ----------------------------- | ---------------------------------------- | ------------------------------ | --------- | | `purchase_invoices.read` | Llistar i llegir factures de compra. | `purchase_invoices:read` | no | | `purchase_invoices.write` | Crear i actualitzar factures de compra. | `purchase_invoices:write` | no | | `purchase_invoices.mark_paid` | Marcar factures de compra com a pagades. | `purchase_invoices:transition` | ⚠ sí | | `purchase_invoices.delete` | Eliminar factures de compra. | `purchase_invoices:delete` | ⚠ sí | **Els scopes de pagament són asimètrics entre vendes i compres.** Registrar un pagament en una factura de **venda** (`register_invoice_payment`) requereix `invoices:write` — edita la factura. En canvi, registrar un pagament en una factura de **compra** (`register_purchase_invoice_payment`) requereix `purchase_invoices:transition`, perquè al costat de compra un pagament fa avançar la factura pel seu cicle de vida (pendent → pagada) en lloc d'editar-la. #### Factures recurrents [#factures-recurrents] | Scope | Concedeix | Maps to | Sensitive | | ------------------------ | ------------------------------------------ | ------------------------------- | --------- | | `recurring.read` | Llistar i llegir plantilles recurrents. | `recurring_invoices:read` | no | | `recurring.write` | Crear i actualitzar plantilles recurrents. | `recurring_invoices:write` | no | | `recurring.pause` | Pausar plantilles recurrents. | `recurring_invoices:transition` | no | | `recurring.resume` | Reprendre plantilles recurrents. | `recurring_invoices:transition` | no | | `recurring.generate_now` | Emetre una factura recurrent manualment. | `recurring_invoices:transition` | ⚠ sí | | `recurring.delete` | Eliminar plantilles recurrents. | `recurring_invoices:delete` | ⚠ sí | #### Compliment (VeriFactu) [#compliment-verifactu] | Scope | Concedeix | Maps to | Sensitive | | ---------------- | ------------------------------------------------------------------------- | ---------------- | --------- | | `verifactu.read` | Llegir registres, esdeveniments, certificats i configuració de VeriFactu. | `verifactu:read` | no | #### Webhooks [#webhooks] | Scope | Concedeix | Maps to | Sensitive | | ----------------- | ---------------------------------------------------------- | ----------------- | --------- | | `webhooks.read` | Llistar webhook endpoints i lliuraments. | `webhooks:read` | no | | `webhooks.write` | Crear, actualitzar, rotar i fer ping de webhook endpoints. | `webhooks:write` | ⚠ sí | | `webhooks.delete` | Eliminar webhook endpoints. | `webhooks:delete` | ⚠ sí | #### Personal — control horari [#personal--control-horari] Dades d'empleats, fitxatges, absències, horaris de treball, presència, festius i exportacions de nòmina. Tots els scopes de personal són **sensibles** (PII d'empleat i dades de compliment) i requereixen el mòdul de pla `control_horario` — consulta [Gating per pla i mòdul](#plan--module-gating). Les lectures, `employees.write` i la generació d'exportacions de nòmina es concedeixen a la pantalla de consentiment; les accions privilegiades d'escriptura i transició no tenen scope OAuth amb punt i són només API key (llistades més avall als scopes detallats). | Scope | Concedeix | Maps to | Sensitive | | ----------------------- | ------------------------------------------------------ | ----------------------- | --------- | | `employees.read` | Llistar i llegir empleats. | `employees:read` | ⚠ sí | | `employees.write` | Crear i actualitzar empleats. | `employees:write` | ⚠ sí | | `time_entries.read` | Llegir fitxatges, saldos i fulls d'hores mensuals. | `time_entries:read` | ⚠ sí | | `absences.read` | Llistar i llegir absències, polítiques i sol·licituds. | `absences:read` | ⚠ sí | | `work_schedules.read` | Llegir horaris de treball i les seves assignacions. | `work_schedules:read` | ⚠ sí | | `presence.read` | Llegir la presència en viu i diària. | `presence:read` | ⚠ sí | | `holidays.read` | Llegir el calendari de festius de l'empresa. | `holidays:read` | ⚠ sí | | `payroll_exports.read` | Llegir les exportacions de nòmina generades. | `payroll_exports:read` | ⚠ sí | | `payroll_exports.write` | Generar exportacions de nòmina. | `payroll_exports:write` | ⚠ sí | ### Macros [#macros] Paquets de conveniència que s'expandeixen a una llista de scopes simples en el moment d'emetre el token. El token persisteix els scopes **expandits** — les macros mai s'emmagatzemen. | Macro | Concedeix | Sensitive | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `factuarea.read` | Accés de lectura complet a tot (sense escriptures). | no | | `factuarea.write` | Llegir-ho tot, a més de crear/actualitzar documents i enviar emails. | no | | `factuarea.full` | Llegir, escriure, enviar i accions destructives (eliminar, anul·lar, marcar com a pagada, signar). Exclou les escriptures de VeriFactu. | ⚠ sí | ## El super-scope `*` [#el-super-scope-] Una credencial que té `*` cobreix **tots** els scopes — les tools en el cas d'una API key. És l'equivalent a una clau de propietari. Reserva'l per a migracions puntuals o automatitzacions de propietari totalment fiables; per a tota la resta, prefereix el conjunt de scopes més reduït. El super-scope està disponible per a les API keys; el consentiment OAuth concedeix scopes explícits (o macros), mai un `*` directe. ## Com els scopes OAuth es converteixen en scopes detallats [#com-els-scopes-oauth-es-converteixen-en-scopes-detallats] Quan s'emet un token OAuth, els seus scopes amb punt es tradueixen un cop al catàleg detallat que apliquen les tools. Val la pena conèixer algunes reconciliacions: * `recurring.*` es mapeja al recurs `recurring_invoices:*`. * `invoices.create_corrective` es mapeja a `invoices:write` (crear és una escriptura). * `invoices.annul` es mapeja a `invoices:void`. * Les accions de cicle de vida (`*.convert`, `*.sign`, `*.pause`, `*.resume`, `*.generate_now`, `*.mark_paid`, `quotes.convert_to_invoice`) es mapegen al scope `:transition` del recurs. * Qualsevol scope de lectura sobre un document també concedeix les utilitats de lectura transversals `pdfs:read` (descarregar el seu PDF/rebut) i `events:read` (el seu registre d'activitat). * `verifactu.write` i `delivery_notes:gdpr_forget` **no** tenen scope OAuth amb punt — són inabastables via OAuth per disseny. * `facturae:read` / `facturae:write` encara **no són al catàleg de consentiment OAuth** — les tools de FacturaE (FACe) només són accessibles amb API key per ara. ## Scopes detallats sense scope OAuth (només API key) [#scopes-detallats-sense-scope-oauth-només-api-key] Alguns scopes detallats viuen al catàleg tancat `recurs:accio` que fan servir les API keys, però **no tenen equivalent OAuth amb punt** — mai es concedeixen a través d'una pantalla de consentiment de tercers i només són accessibles amb API key. Concedeix-los directament a la key (o via el super-scope `*`). Alguns estan gateats per un mòdul d'integració —llavors l'empresa de la key ha de tenir el pla corresponent (consulta [Gating per pla i mòdul](#plan--module-gating))—; la resta són scopes de compte propi i de gestoria. | Scope | Concedeix | Gating de mòdul | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `stripe_autoinvoicing:read` | Llegir l'estat de la integració Stripe Connect, la configuració d'auto-facturació i els comptes connectats, i llistar cobraments/rectificatives auto-facturats. | `integration_stripe` | | `stripe_autoinvoicing:write` | Activar/desactivar l'auto-facturació de cobraments Stripe, fixar la sèrie auto-emesa i editar/desconnectar comptes connectats. | `integration_stripe` | | `payouts:read` | Llegir els payouts de Stripe ingerits i el seu estat de conciliació bancària. | `integration_stripe` | Aquests scopes habiliten les [tools de Pagaments i passarel·les](/mcp/tools#payments). Un segon grup de scopes només per a API key governa la gestió de **compte propi** i de **gestoria** — les teves pròpies credencials i, per a gestories, les empreses filles que gestiones i les seves API keys. `companies:*` requereix el mòdul del pla de gestoria; la resta no tenen gating de mòdul. | Scope | Concedeix | Gating de mòdul | | ------------------ | --------------------------------------------------------------------------------------------------------- | --------------- | | `account:write` | Gestionar les teves pròpies API keys (crear, rotar, revocar) i actualitzar la personalització del compte. | — | | `companies:read` | Llistar i llegir les empreses gestionades (subcomptes fills). | `gestoria` | | `companies:write` | Crear, actualitzar, activar i desactivar empreses gestionades. | `gestoria` | | `companies:delete` | Arxivar empreses gestionades. | `gestoria` | | `api_keys:read` | Llistar i llegir les API keys de les empreses gestionades. | — | | `api_keys:write` | Crear, rotar i revocar les API keys de les empreses gestionades. | — | | `api_keys:delete` | Eliminar permanentment les API keys de les empreses gestionades. | — | Un tercer grup cobreix les accions d'escriptura i transició de **personal** (control horari). Les seves lectures es concedeixen per OAuth (consulta els scopes de consentiment de Personal més amunt), però aquests scopes privilegiats no tenen equivalent OAuth amb punt — són només API key, el mirall de `verifactu:write`. Tots requereixen el mòdul de pla `control_horario`. | Scope | Concedeix | Gating de mòdul | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------- | | `employees:delete` | Eliminar empleats de manera permanent. | `control_horario` | | `time_entries:write` | Fitxar entrada/sortida, registrar entrades manuals, gestionar correccions de fitxatge i el tancament mensual del registre. | `control_horario` | | `absences:write` | Crear i gestionar tipus, polítiques i sol·licituds d'absència. | `control_horario` | | `absences:transition` | Aprovar, rebutjar i cancel·lar sol·licituds d'absència. | `control_horario` | | `work_schedules:write` | Crear, actualitzar, assignar i arxivar horaris de treball. | `control_horario` | `verifactu:write`, `facturae:read`, `facturae:write` i `delivery_notes:gdpr_forget` també són scopes detallats només per a API key (descrits a dalt) — s'apliquen a la frontera de la tool com qualsevol altre scope, però no tenen contrapart al consentiment OAuth. ## Gating per pla i mòdul [#gating-per-pla-i-mòdul] La majoria de les tools publicades només apliquen una comprovació de **scope**: són accessibles quan la credencial té el scope requerit. Dues famílies a més estan **limitades per mòdul**. Les [tools de Pagaments i passarel·les](/mcp/tools#payments) mapegen els seus scopes (`stripe_autoinvoicing:*`, `payouts:read`) al mòdul `integration_stripe`. Les tools de personal (Empleats, Places d'empleat, Horaris de treball, Control horari, Absències, Presència, Festius) mapegen els seus scopes (`employees:*`, `time_entries:*`, `absences:*`, `work_schedules:*`, `presence:read`, `holidays:read`, `payroll_exports:*`) al mòdul `control_horario`. Quan el pla de l'empresa no inclou el mòdul, el servidor oculta aquestes tools de `tools/list` i retorna `module_not_in_plan` (`-32005`) davant d'una crida directa. * Els **límits d'ús** del pla (p. ex. quotes mensuals de documents) s'apliquen en el moment de la crida i es manifesten com a `plan_limit_exceeded` (`-32004`). Consulta [Errors i límits de peticions](/mcp/errors). Tota la superfície pública MCP també requereix que l'empresa tingui un **pla de Factuarea actiu** — l'accés a l'API està inclòs en tots els plans; en cas contrari, cada crida retorna `addon_not_active` (`-32007`). --- # Catàleg de tools (/ca/mcp/tools) El servidor MCP de Factuarea publica ** tools** en 27 dominis. Aquesta pàgina és la llista canònica; l'esquema d'entrada de cada tool es descobreix en temps d'execució mitjançant `tools/list`. Cada tool es correspon amb la mateixa operació a la Referència de la REST API. ## Com llegir aquest catàleg [#com-llegir-aquest-catàleg] * **Tool** — el nom de la tool MCP que invoca el teu agent. * **Scope** — el scope granular que ha de tenir la credencial perquè la tool aparegui a `tools/list` i sigui invocable. Consulta [Scopes i permisos](/mcp/scopes). * **Category** — el bucket de límit de peticions al qual pertany la tool: `read`, `write`, `destructive`, `send` o `generate`. Consulta [Errors i límits de peticions](/mcp/errors). Una credencial només veu les tools que cobreixen els seus scopes. Una API key amb el super-scope `*` les veu totes ; una key més restringida o un token OAuth en veu un subconjunt. **¹ Restringides a OAuth (només API key).** tools són accessibles **únicament** amb una API key, mai a través d'una app OAuth de tercers: les 8 tools `verifactu:write`, `forget_delivery_note_signature` (`delivery_notes:gdpr_forget`), les 5 tools de FacturaE (els scopes `facturae:read` / `facturae:write` encara no són al catàleg de consentiment OAuth), les 13 tools de Pagaments i passarel·les (els scopes `stripe_autoinvoicing:*`, `payouts:read` i `integration_events:*` són scopes granulars, només API key), les 3 tools de Correus enviats i les 2 de registre de peticions (`emails:read` i `developers:read` inspeccionen el trànsit de la teva pròpia integració: són scopes de primera part, mai es concedeixen per consentiment de tercers), les 17 tools de Gestoria (`companies:*` i `api_keys:*`: gestionar sub-comptes i credencials mai es concedeix a tercers per consentiment OAuth — inclou `get_consolidated_workforce`), les 4 tools d'escriptura de compte (`account:write`: crear, rotar o revocar API keys i actualitzar la personalització del compte) i les 33 tools de control horari els scopes d'escriptura/transició de les quals (`time_entries:write`, `absences:write`, `absences:transition`, `work_schedules:write`) són granulars i només API key, fora del catàleg de consentiment OAuth (les seves lectures i `employees:write` sí es concedeixen per OAuth). Per tant, les apps OAuth arriben a **** de les tools. Consulta la [política de canals](/mcp/connect#channel-policy). ## Dominis [#dominis] ### Factures [#invoice] Factures de venda: cerca, CRUD, rectificatives, ledger de pagaments parcials, transicions (paid/sent/void/annul), programació, operacions massives, recordatoris, enllaços públics, PDFs i exportació a Excel. *(43 tools)* | Eina | Scope | Categoria | | -------------------------------------- | ----------------- | ----------- | | `can_annul_invoice` | `invoices:read` | read | | `check_invoice_simplified_eligibility` | `invoices:read` | read | | `export_invoices_excel` | `invoices:read` | read | | `find_invoice_by_external_id` | `invoices:read` | read | | `find_invoice_by_number` | `invoices:read` | read | | `get_available_quarters` | `invoices:read` | read | | `get_invoice` | `invoices:read` | read | | `get_invoice_activities` | `invoices:read` | read | | `get_invoice_correctives` | `invoices:read` | read | | `get_invoice_payment_receipt` | `pdfs:read` | read | | `get_invoice_pdf` | `pdfs:read` | read | | `get_invoice_public_link` | `invoices:read` | read | | `get_invoice_stats` | `invoices:read` | read | | `get_invoice_statuses` | `invoices:read` | read | | `list_invoice_payments` | `invoices:read` | read | | `list_payment_methods` | `invoices:read` | read | | `preview_invoice_reminder` | `invoices:read` | read | | `search_invoices` | `invoices:read` | read | | `annul_invoice` | `invoices:void` | write | | `assign_invoice_real_number` | `invoices:write` | write | | `bulk_change_invoice_status` | `invoices:write` | write | | `bulk_create_invoices` | `invoices:write` | write | | `bulk_delete_invoices` | `invoices:delete` | destructive | | `bulk_invoices_pdf_link` | `pdfs:read` | generate | | `bulk_send_invoices` | `invoices:send` | send | | `create_corrective_invoice` | `invoices:write` | write | | `create_invoice` | `invoices:write` | write | | `delete_invoice` | `invoices:delete` | destructive | | `duplicate_invoice` | `invoices:write` | write | | `mark_invoice_as_paid` | `invoices:write` | write | | `mark_invoice_as_sent` | `invoices:write` | write | | `mark_invoice_unsent` | `invoices:write` | write | | `quarterly_send_email` | `invoices:send` | send | | `register_invoice_payment` | `invoices:write` | write | | `reschedule_invoice` | `invoices:write` | write | | `schedule_invoice` | `invoices:write` | write | | `send_invoice` | `invoices:send` | send | | `send_invoice_reminder` | `invoices:send` | send | | `substitute_simplified_invoice` | `invoices:write` | write | | `unschedule_invoice` | `invoices:write` | write | | `update_invoice` | `invoices:write` | write | | `update_invoice_public_link` | `invoices:write` | write | | `void_invoice` | `invoices:void` | write | ### Clients [#client] CRM de clients: cerca, CRUD, creació/esborrat massiu, cerca per NIF o ID extern, importació CSV, verificació censal AEAT, estadístiques i activitat. *(13 tools)* | Eina | Scope | Categoria | | ---------------------------- | ---------------- | ----------- | | `find_client_by_external_id` | `clients:read` | read | | `find_client_by_tax_id` | `clients:read` | read | | `get_client` | `clients:read` | read | | `get_client_activities` | `clients:read` | read | | `get_client_stats` | `clients:read` | read | | `search_clients` | `clients:read` | read | | `verify_client_census` | `clients:read` | read | | `bulk_create_clients` | `clients:write` | write | | `bulk_delete_clients` | `clients:delete` | destructive | | `create_client` | `clients:write` | write | | `delete_client` | `clients:delete` | destructive | | `import_clients_csv` | `clients:write` | write | | `update_client` | `clients:write` | write | ### Proveïdors [#supplier] CRM de proveïdors: cerca, CRUD, esborrat/estat massiu, cerca per NIF o ID extern, activar/desactivar, estadístiques i activitat. *(12 tools)* | Eina | Scope | Categoria | | ------------------------------ | ------------------ | ----------- | | `find_supplier_by_external_id` | `suppliers:read` | read | | `find_supplier_by_tax_id` | `suppliers:read` | read | | `get_supplier` | `suppliers:read` | read | | `get_supplier_activities` | `suppliers:read` | read | | `get_supplier_stats` | `suppliers:read` | read | | `search_suppliers` | `suppliers:read` | read | | `bulk_change_supplier_status` | `suppliers:write` | write | | `bulk_delete_suppliers` | `suppliers:delete` | destructive | | `create_supplier` | `suppliers:write` | write | | `delete_supplier` | `suppliers:delete` | destructive | | `toggle_supplier_active` | `suppliers:write` | write | | `update_supplier` | `suppliers:write` | write | ### Productes [#product] Catàleg: cerca, CRUD, stock/esborrat/estat massiu, cerca per SKU o ID extern, analítica de vendes, informes d'estoc baix i media (galeria/vídeo). *(21 tools)* | Eina | Scope | Categoria | | -------------------------------- | ----------------- | ----------- | | `download_product_gallery_image` | `products:read` | read | | `download_product_video` | `products:read` | read | | `find_product_by_external_id` | `products:read` | read | | `find_product_by_sku` | `products:read` | read | | `get_product` | `products:read` | read | | `get_product_activities` | `products:read` | read | | `get_product_sales_analytics` | `products:read` | read | | `get_product_stats` | `products:read` | read | | `low_stock_report` | `products:read` | read | | `search_products` | `products:read` | read | | `bulk_change_product_status` | `products:write` | write | | `bulk_delete_products` | `products:delete` | destructive | | `bulk_update_stock` | `products:write` | write | | `create_product` | `products:write` | write | | `delete_product` | `products:delete` | destructive | | `delete_product_gallery_image` | `products:delete` | destructive | | `delete_product_video` | `products:delete` | destructive | | `toggle_product_active` | `products:write` | write | | `update_product` | `products:write` | write | | `upload_product_gallery_image` | `products:write` | write | | `upload_product_video` | `products:write` | write | ### Pressupostos [#quote] Pressupostos: cerca, CRUD, acceptar/rebutjar, enviar, convertir a factura, operacions massives, cerca per ID extern, enllaços públics i PDFs. *(20 tools)* | Eina | Scope | Categoria | | --------------------------- | ------------------- | ----------- | | `find_quote_by_external_id` | `quotes:read` | read | | `get_quote` | `quotes:read` | read | | `get_quote_pdf` | `pdfs:read` | read | | `get_quote_public_link` | `quotes:read` | read | | `get_quote_stats` | `quotes:read` | read | | `get_quote_statuses` | `quotes:read` | read | | `search_quotes` | `quotes:read` | read | | `accept_quote` | `quotes:transition` | write | | `bulk_change_quote_status` | `quotes:transition` | write | | `bulk_delete_quotes` | `quotes:delete` | destructive | | `bulk_quotes_pdf_link` | `pdfs:read` | generate | | `bulk_send_quotes` | `quotes:send` | send | | `convert_quote` | `quotes:transition` | write | | `create_quote` | `quotes:write` | write | | `delete_quote` | `quotes:delete` | destructive | | `duplicate_quote` | `quotes:write` | write | | `reject_quote` | `quotes:transition` | write | | `send_quote` | `quotes:send` | send | | `update_quote` | `quotes:write` | write | | `update_quote_public_link` | `quotes:write` | write | ### Factures proforma [#proforma] Proformes: cerca, CRUD, acceptar/rebutjar, enviar, convertir, operacions massives, cerca per ID extern, enllaços públics i PDFs. *(20 tools)* | Eina | Scope | Categoria | | ------------------------------ | ---------------------- | ----------- | | `find_proforma_by_external_id` | `proformas:read` | read | | `get_proforma` | `proformas:read` | read | | `get_proforma_pdf` | `pdfs:read` | read | | `get_proforma_public_link` | `proformas:read` | read | | `get_proforma_stats` | `proformas:read` | read | | `get_proforma_statuses` | `proformas:read` | read | | `search_proformas` | `proformas:read` | read | | `accept_proforma` | `proformas:transition` | write | | `bulk_change_proforma_status` | `proformas:transition` | write | | `bulk_delete_proformas` | `proformas:delete` | destructive | | `bulk_proformas_pdf_link` | `pdfs:read` | generate | | `bulk_send_proformas` | `proformas:send` | send | | `convert_proforma` | `proformas:transition` | write | | `create_proforma` | `proformas:write` | write | | `delete_proforma` | `proformas:delete` | destructive | | `duplicate_proforma` | `proformas:write` | write | | `reject_proforma` | `proformas:transition` | write | | `send_proforma` | `proformas:send` | send | | `update_proforma` | `proformas:write` | write | | `update_proforma_public_link` | `proformas:write` | write | ### Albarans [#delivery-note] Albarans: cerca, CRUD, lliurar/cancel·lar/signar, convertir, enviar, operacions massives, cerca per ID extern, enllaços públics, PDFs i esborrat GDPR de la signatura. *(22 tools)* | Eina | Scope | Categoria | | ----------------------------------- | ------------------------------ | ----------- | | `find_delivery_note_by_external_id` | `delivery_notes:read` | read | | `get_delivery_note` | `delivery_notes:read` | read | | `get_delivery_note_pdf` | `pdfs:read` | read | | `get_delivery_note_public_link` | `delivery_notes:read` | read | | `get_delivery_note_stats` | `delivery_notes:read` | read | | `get_delivery_note_statuses` | `delivery_notes:read` | read | | `search_delivery_notes` | `delivery_notes:read` | read | | `bulk_change_delivery_note_status` | `delivery_notes:transition` | write | | `bulk_delete_delivery_notes` | `delivery_notes:delete` | destructive | | `bulk_delivery_notes_pdf_link` | `pdfs:read` | generate | | `bulk_send_delivery_notes` | `delivery_notes:write` | send | | `cancel_delivery_note` | `delivery_notes:transition` | write | | `convert_delivery_note` | `delivery_notes:transition` | write | | `create_delivery_note` | `delivery_notes:write` | write | | `delete_delivery_note` | `delivery_notes:delete` | destructive | | `duplicate_delivery_note` | `delivery_notes:write` | write | | `forget_delivery_note_signature` | `delivery_notes:gdpr_forget` ¹ | destructive | | `mark_delivery_note_delivered` | `delivery_notes:transition` | write | | `send_delivery_note` | `delivery_notes:write` | send | | `sign_delivery_note` | `delivery_notes:transition` | write | | `update_delivery_note` | `delivery_notes:write` | write | | `update_delivery_note_public_link` | `delivery_notes:write` | write | ### Factures de compra [#purchase-invoice] Factures de compra: cerca, CRUD, marcar com a pagada, ledger de pagaments parcials, operacions massives, llistes de pendents/vençudes, cerca per ID extern, adjunts de fitxer i rebuts de pagament. *(18 tools)* | Eina | Scope | Categoria | | -------------------------------------- | ------------------------------ | ----------- | | `download_purchase_invoice_file` | `purchase_invoices:read` | read | | `find_purchase_invoice_by_external_id` | `purchase_invoices:read` | read | | `get_purchase_invoice` | `purchase_invoices:read` | read | | `get_purchase_invoice_payment_receipt` | `pdfs:read` | read | | `get_purchase_invoice_stats` | `purchase_invoices:read` | read | | `list_overdue_purchase_invoices` | `purchase_invoices:read` | read | | `list_pending_purchase_invoices` | `purchase_invoices:read` | read | | `list_purchase_invoice_payments` | `purchase_invoices:read` | read | | `search_purchase_invoices` | `purchase_invoices:read` | read | | `attach_purchase_invoice_file` | `purchase_invoices:write` | write | | `bulk_change_purchase_invoice_status` | `purchase_invoices:transition` | write | | `bulk_delete_purchase_invoices` | `purchase_invoices:delete` | destructive | | `create_purchase_invoice` | `purchase_invoices:write` | write | | `delete_purchase_invoice` | `purchase_invoices:delete` | destructive | | `delete_purchase_invoice_file` | `purchase_invoices:write` | write | | `mark_purchase_invoice_paid` | `purchase_invoices:transition` | write | | `register_purchase_invoice_payment` | `purchase_invoices:transition` | write | | `update_purchase_invoice` | `purchase_invoices:write` | write | ### Factures recurrents [#recurring-invoice] Plantilles recurrents: cerca, CRUD, crear des d'una factura, activar/pausar/reprendre/cancel·lar/ometre, previsualització, cerca per ID extern, logs i activitat. *(17 tools)* | Eina | Scope | Categoria | | --------------------------------------- | ------------------------------- | ----------- | | `find_recurring_invoice_by_external_id` | `recurring_invoices:read` | read | | `get_recurring_invoice` | `recurring_invoices:read` | read | | `get_recurring_invoice_stats` | `recurring_invoices:read` | read | | `list_recurring_invoice_activities` | `recurring_invoices:read` | read | | `list_recurring_invoice_logs` | `recurring_invoices:read` | read | | `preview_recurring_invoice` | `recurring_invoices:read` | read | | `search_recurring_invoices` | `recurring_invoices:read` | read | | `activate_recurring_invoice` | `recurring_invoices:transition` | write | | `bulk_delete_recurring_invoices` | `recurring_invoices:delete` | destructive | | `cancel_recurring_invoice` | `recurring_invoices:transition` | write | | `create_recurring_invoice` | `recurring_invoices:write` | write | | `create_recurring_invoice_from_invoice` | `recurring_invoices:write` | write | | `delete_recurring_invoice` | `recurring_invoices:delete` | destructive | | `pause_recurring_invoice` | `recurring_invoices:transition` | write | | `resume_recurring_invoice` | `recurring_invoices:transition` | write | | `skip_recurring_invoice` | `recurring_invoices:write` | write | | `update_recurring_invoice` | `recurring_invoices:write` | write | ### Sèries [#series] Sèries de numeració (immutables per continuïtat fiscal): cerca, crear, arxivar/desarxivar, marcar per defecte, arrencar les quatre sèries per defecte d'una empresa acabada de donar d'alta, estadístiques i activitat. *(12 tools)* | Eina | Scope | Categoria | | ----------------------------- | -------------- | --------- | | `find_series_by_code` | `series:read` | read | | `get_default_series_for_type` | `series:read` | read | | `get_series` | `series:read` | read | | `get_series_activities` | `series:read` | read | | `get_series_stats` | `series:read` | read | | `list_active_series` | `series:read` | read | | `search_series` | `series:read` | read | | `archive_series` | `series:write` | write | | `bootstrap_series` | `series:write` | write | | `create_series` | `series:write` | write | | `mark_series_as_default` | `series:write` | write | | `unarchive_series` | `series:write` | write | ### Impostos [#tax] Tipus impositius (catàleg global): cerca, CRUD-lite, valors per defecte, comprovacions d'ús, càlculs d'impost/totals i el catàleg fiscal AEAT de només lectura (règims, causes d'exempció, tipus d'IRPF i els parells legals IVA ↔ recàrrec d'equivalència). *(16 tools)* | Eina | Scope | Categoria | | ------------------------------- | ------------- | --------- | | `calculate_tax` | `taxes:read` | read | | `calculate_totals` | `taxes:read` | read | | `check_tax_in_use` | `taxes:read` | read | | `get_active_taxes` | `taxes:read` | read | | `get_tax` | `taxes:read` | read | | `get_tax_catalog` | `taxes:read` | read | | `get_tax_defaults_for_document` | `taxes:read` | read | | `get_tax_stats` | `taxes:read` | read | | `get_taxes_by_type` | `taxes:read` | read | | `get_taxes_for_purchases` | `taxes:read` | read | | `get_taxes_for_sales` | `taxes:read` | read | | `search_taxes` | `taxes:read` | read | | `create_tax` | `taxes:write` | write | | `set_tax_as_default` | `taxes:write` | write | | `set_tax_default_for_document` | `taxes:write` | write | | `toggle_tax_active` | `taxes:write` | write | ### VeriFactu [#verifactu] SIF de l'AEAT: registres, esdeveniments, validació de cadena, reintent, subsanació de registres rebutjats, certificats, ajustos, declaració responsable i log d'accés a l'AEAT. *(27 tools)* | Eina | Scope | Categoria | | ----------------------------------------- | ------------------- | --------- | | `find_verifactu_record_by_csv` | `verifactu:read` | read | | `find_verifactu_record_by_huella` | `verifactu:read` | read | | `find_verifactu_record_by_invoice_number` | `verifactu:read` | read | | `get_active_company_certificate` | `verifactu:read` | read | | `get_aeat_access_record` | `verifactu:read` | read | | `get_declaracion_responsable` | `verifactu:read` | read | | `get_declaracion_responsable_history` | `verifactu:read` | read | | `get_invoice_verifactu` | `verifactu:read` | read | | `get_verifactu_activities` | `verifactu:read` | read | | `get_verifactu_config` | `verifactu:read` | read | | `get_verifactu_event` | `verifactu:read` | read | | `get_verifactu_event_summary` | `verifactu:read` | read | | `get_verifactu_record` | `verifactu:read` | read | | `get_verifactu_stats` | `verifactu:read` | read | | `list_aeat_access_records` | `verifactu:read` | read | | `list_company_certificates` | `verifactu:read` | read | | `list_verifactu_events` | `verifactu:read` | read | | `search_verifactu_records` | `verifactu:read` | read | | `validate_verifactu_chain` | `verifactu:read` | read | | `activate_company_certificate` | `verifactu:write` ¹ | write | | `create_invoice_verifactu` | `verifactu:write` ¹ | write | | `retry_verifactu_event` | `verifactu:write` ¹ | write | | `retry_verifactu_record` | `verifactu:write` ¹ | write | | `revoke_company_certificate` | `verifactu:write` ¹ | write | | `subsanar_verifactu_record` | `verifactu:write` ¹ | write | | `update_verifactu_settings` | `verifactu:write` ¹ | write | | `upload_company_certificate` | `verifactu:write` ¹ | write | ### FacturaE (FACe) [#facturae] Facturació electrònica B2G: descarrega el XML FacturaE 3.2.2 d'una factura i gestiona les seves presentacions a FACe (enviar, seguir, cancel·lar). *(5 tools)* | Eina | Scope | Categoria | | ------------------------------- | ------------------ | --------- | | `get_face_submission` | `facturae:read` ¹ | read | | `list_invoice_face_submissions` | `facturae:read` ¹ | read | | `cancel_face_submission` | `facturae:write` ¹ | write | | `get_invoice_facturae_link` | `facturae:read` ¹ | generate | | `send_invoice_to_face` | `facturae:write` ¹ | write | ### Webhooks i esdeveniments [#webhook] Endpoints de webhook, lliuraments, rotació de secret, ping/replay, esdeveniments de prova i el catàleg d'esdeveniments publicats. *(13 tools)* | Eina | Scope | Categoria | | -------------------------- | ----------------- | --------- | | `get_event` | `events:read` | read | | `get_webhook_delivery` | `webhooks:read` | read | | `get_webhook_endpoint` | `webhooks:read` | read | | `list_events` | `events:read` | read | | `list_webhook_deliveries` | `webhooks:read` | read | | `search_webhook_endpoints` | `webhooks:read` | read | | `create_webhook_endpoint` | `webhooks:write` | write | | `delete_webhook_endpoint` | `webhooks:delete` | write | | `ping_webhook_endpoint` | `webhooks:write` | write | | `replay_webhook_delivery` | `webhooks:write` | write | | `rotate_webhook_secret` | `webhooks:write` | write | | `test_webhook_endpoint` | `webhooks:write` | write | | `update_webhook_endpoint` | `webhooks:write` | write | ### Compte [#account] Identitat fiscal i personalització del compte: verifica el par nom + NIF registrat de l'empresa contra el cens de l'AEAT, consulta plantilles de personalització i actualitza la personalització del compte. *( tools)* | Eina | Scope | Categoria | | --------------------------------------- | ----------------- | --------- | | `get_account_billing` | `account:read` | read | | `get_account_personalization_templates` | `account:read` | read | | `update_account_personalization` | `account:write` ¹ | write | | `verify_account_census` | `account:read` | write | ### API keys [#api-key] API keys self-service del teu propi tenant: llistar, crear, consultar, rotar el secret i revocar. El secret es retorna un sol cop, en crear i en rotar. *(5 tools)* | Eina | Scope | Categoria | | ----------------------- | ----------------- | --------- | | `get_api_key` | `account:read` | read | | `list_api_keys` | `account:read` | read | | `create_api_key` | `account:write` ¹ | write | | `revoke_api_key` | `account:write` ¹ | write | | `rotate_api_key_secret` | `account:write` ¹ | write | ### Gestoria [#gestoria] Mode gestoria: gestiona les empreses filles del tenant mestre i les seves child API keys — crear/llistar/consultar/actualitzar/eliminar, activar/desactivar, previsualització del cost per plaça, estat d'aprovisionament, API keys per empresa i un resum consolidat del compliment horari de les empreses filles. *(17 tools)* | Eina | Scope | Categoria | | --------------------------------- | -------------------- | --------- | | `get_company` | `companies:read` ¹ | read | | `get_company_api_key` | `api_keys:read` ¹ | read | | `get_company_creation_status` | `companies:read` ¹ | read | | `get_company_seat_charge_preview` | `companies:read` ¹ | read | | `get_consolidated_workforce` | `companies:read` ¹ | read | | `list_companies` | `companies:read` ¹ | read | | `list_company_api_keys` | `api_keys:read` ¹ | read | | `activate_companies` | `companies:write` ¹ | write | | `activate_company` | `companies:write` ¹ | write | | `create_company` | `companies:write` ¹ | write | | `create_company_api_key` | `api_keys:write` ¹ | write | | `deactivate_company` | `companies:write` ¹ | write | | `delete_company` | `companies:delete` ¹ | write | | `revoke_company_api_key` | `api_keys:write` ¹ | write | | `rotate_company_api_key_secret` | `api_keys:write` ¹ | write | | `update_company` | `companies:write` ¹ | write | | `verify_company_creation` | `companies:write` ¹ | write | ### Pagaments i passarel·les [#payments] Auto-facturació de passarel·les de pagament i safata d'esdeveniments de les passarel·les: estat i configuració de Stripe Connect, comptes connectats (multi-botiga), cobraments i rectificatives auto-facturats, payouts de Stripe, i els esdeveniments que les passarel·les han enviat a Factuarea — què va arribar, què va produir i, quan no va produir res, el motiu tipat. És el lloc on mirar quan un cobrament no ha generat factura. Avui només Stripe està disponible (GoCardless i MONEI encara no ho estan). Aquestes tools són **només API key** — els seus scopes no són al catàleg de consentiment OAuth. Les d'auto-facturació i payouts de Stripe estan a més gated pel mòdul `integration_stripe` (pla Empresari en endavant); les de la safata d'esdeveniments, no. *(13 tools)* `replay_integrations_event` té efecte fiscal real. Si la causa que va impedir facturar ja està resolta, reprocessar un esdeveniment aparcat **pot emetre una factura de debò**, amb el seu número de sèrie i el seu alta a VeriFactu. No és un reintent innocu: confirma-ho amb la persona usuària abans d'invocar-la. No duplica factures: el reprocés torna a passar per la mateixa comprovació d'idempotència de l'intent original. | Eina | Scope | Categoria | | -------------------------------------- | ------------------------------ | --------- | | `get_integrations_event` | `integration_events:read` ¹ | read | | `get_payout` | `payouts:read` ¹ | read | | `get_stripe_autoinvoicing_config` | `stripe_autoinvoicing:read` ¹ | read | | `get_stripe_connected_account` | `stripe_autoinvoicing:read` ¹ | read | | `list_integrations_events` | `integration_events:read` ¹ | read | | `list_stripe_autoinvoiced_correctives` | `stripe_autoinvoicing:read` ¹ | read | | `list_stripe_autoinvoiced_payments` | `stripe_autoinvoicing:read` ¹ | read | | `list_stripe_connected_accounts` | `stripe_autoinvoicing:read` ¹ | read | | `search_payouts` | `payouts:read` ¹ | read | | `disconnect_stripe_connected_account` | `stripe_autoinvoicing:write` ¹ | write | | `replay_integrations_event` | `integration_events:write` ¹ | write | | `update_stripe_autoinvoicing_config` | `stripe_autoinvoicing:write` ¹ | write | | `update_stripe_connected_account` | `stripe_autoinvoicing:write` ¹ | write | ### Correus enviats [#email] Registre de correus enviats: llista els correus que Factuarea ha enviat en nom de l'empresa (factures, pressupostos, albarans, recordatoris de pagament), consulta'n un pel seu id i resumeix d'una sola vegada com va acabar l'enviament d'un lot de fins a 100 documents. Fes-lo servir per respondre «s'ha enviat el correu d'aquesta factura?» i per investigar enviaments fallits. *(3 tools)* L'estat descriu el **lliurament al servidor SMTP de sortida, no el lliurament real**. `sent` significa que el servidor de correu de sortida va acceptar el missatge: tot i així pot rebotar o acabar a spam sense que Factuarea se n'assabenti. No existeixen els estats `delivered`, `bounced` ni `opened`; `queued`, `sending`, `sent` i `failed` són els únics. No afirmis mai que la persona destinatària el va rebre, el va obrir o el va llegir. | Eina | Scope | Categoria | | ----------------------- | --------------- | --------- | | `get_emails` | `emails:read` ¹ | read | | `get_emails_indicators` | `emails:read` ¹ | read | | `list_emails` | `emails:read` ¹ | read | ### Registre de peticions a l'API [#request-log] El trànsit de la teva pròpia integració contra l'API pública v1 dels últims 30 dies: llista les crides amb el seu mètode, ruta, codi d'estat, durada, prefix d'API key i entorn, amb filtres per només errors o per qualsevol d'aquests camps, i consulta'n una de concreta pel seu `request_id`, l'identificador opac que l'API retorna a la capçalera de cada resposta. Fes-lo servir per depurar una integració: què va cridar, quan, amb quin codi d'estat i quant va trigar. Les capçaleres, el cos i la query string **no** s'emmagatzemen i no es retornen mai. *(2 tools)* | Eina | Scope | Categoria | | ------------------------------ | ------------------- | --------- | | `get_developers_request_logs` | `developers:read` ¹ | read | | `list_developers_request_logs` | `developers:read` ¹ | read | ### Empleats [#employee] Plantilla de personal: cerca, alta/edició/baixa/reactivació, estadístiques i el cicle d'invitacions (enviar/reenviar/cancel·lar i llistar invitacions d'empleat). Requereix el mòdul `control_horario`. *(12 tools)* | Eina | Scope | Categoria | | ------------------------------ | ----------------- | --------- | | `find_employee_by_external_id` | `employees:read` | read | | `get_employee` | `employees:read` | read | | `get_employee_stats` | `employees:read` | read | | `list_employee_invitations` | `employees:read` | read | | `search_employees` | `employees:read` | read | | `cancel_employee_invitation` | `employees:write` | write | | `create_employee` | `employees:write` | write | | `deactivate_employee` | `employees:write` | write | | `reactivate_employee` | `employees:write` | write | | `resend_employee_invitation` | `employees:write` | write | | `send_employee_invitation` | `employees:write` | write | | `update_employee` | `employees:write` | write | ### Places d'empleat [#employee-seat] Facturació de l'add-on de places: la subscripció de places per contracte — previsualitzar i consultar el càrrec i la facturació de la plaça, subscriure, canviar la quantitat de places i cancel·lar l'add-on. Requereix el mòdul `control_horario`. *(5 tools)* | Eina | Scope | Categoria | | ------------------------------- | ----------------- | --------- | | `get_employee_seat_billing` | `employees:read` | read | | `preview_employee_seat_charge` | `employees:read` | read | | `cancel_employee_seat_addon` | `employees:write` | write | | `change_employee_seat_quantity` | `employees:write` | write | | `subscribe_employee_seat_addon` | `employees:write` | write | ### Horaris de treball [#work-schedule] Horaris de treball setmanals i les seves assignacions: cerca, CRUD, arxivar/desarxivar, assignar/desassignar a empleats i consultar l'horari efectiu d'un empleat. Requereix el mòdul `control_horario`. *(11 tools)* | Eina | Scope | Categoria | | -------------------------------- | ------------------------ | --------- | | `get_employee_work_schedule` | `work_schedules:read` | read | | `get_work_schedule` | `work_schedules:read` | read | | `get_work_schedule_stats` | `work_schedules:read` | read | | `list_work_schedule_assignments` | `work_schedules:read` | read | | `search_work_schedules` | `work_schedules:read` | read | | `archive_work_schedule` | `work_schedules:write` ¹ | write | | `assign_work_schedule` | `work_schedules:write` ¹ | write | | `create_work_schedule` | `work_schedules:write` ¹ | write | | `unarchive_work_schedule` | `work_schedules:write` ¹ | write | | `unassign_work_schedule` | `work_schedules:write` ¹ | write | | `update_work_schedule` | `work_schedules:write` ¹ | write | ### Control horari [#time-tracking] Registre de jornada (RD-llei 8/2019): fitxar entrada/sortida amb pauses, entrades manuals, el flux de correccions de fitxatge, saldos i fulls d'hores mensuals, el tancament mensual inalterable del registre (tancar/reobrir/segellar, signatura, validació de cadena) i les seves exportacions, a més de les exportacions de nòmina. Requereix el mòdul `control_horario`. *(29 tools)* | Eina | Scope | Categoria | | ----------------------------------- | ---------------------- | --------- | | `export_closed_register` | `time_entries:read` | read | | `get_current_time_entry_session` | `time_entries:read` | read | | `get_employee_time_balance` | `time_entries:read` | read | | `get_monthly_close_report` | `time_entries:read` | read | | `get_monthly_register_signature` | `time_entries:read` | read | | `get_monthly_time_record_close` | `time_entries:read` | read | | `get_monthly_time_sheet` | `time_entries:read` | read | | `get_team_time_balance_summary` | `time_entries:read` | read | | `get_time_correction` | `time_entries:read` | read | | `get_time_entry` | `time_entries:read` | read | | `get_time_tracking_settings` | `time_entries:read` | read | | `search_monthly_time_record_closes` | `time_entries:read` | read | | `search_time_corrections` | `time_entries:read` | read | | `search_time_entries` | `time_entries:read` | read | | `validate_time_record_chain` | `time_entries:read` | read | | `export_payroll` | `payroll_exports:read` | read | | `list_payroll_export_formats` | `payroll_exports:read` | read | | `approve_time_correction` | `time_entries:write` ¹ | write | | `clock_in` | `time_entries:write` ¹ | write | | `clock_out` | `time_entries:write` ¹ | write | | `close_monthly_time_record` | `time_entries:write` ¹ | write | | `pause_time_entry` | `time_entries:write` ¹ | write | | `record_manual_time_entry` | `time_entries:write` ¹ | write | | `reject_time_correction` | `time_entries:write` ¹ | write | | `reopen_monthly_time_record` | `time_entries:write` ¹ | write | | `request_time_correction` | `time_entries:write` ¹ | write | | `resume_time_entry` | `time_entries:write` ¹ | write | | `seal_monthly_time_record` | `time_entries:write` ¹ | write | | `update_time_tracking_settings` | `time_entries:write` ¹ | write | ### Absències [#absence] Absències: tipus, polítiques (amb configuració de carryover i assignacions), sol·licituds (crear/aprovar/rebutjar/cancel·lar), saldos i el calendari d'absències de l'equip. Requereix el mòdul `control_horario`. *(25 tools)* | Eina | Scope | Categoria | | ------------------------------------ | ----------------------- | --------- | | `get_absence_balance` | `absences:read` | read | | `get_absence_calendar` | `absences:read` | read | | `get_absence_policy` | `absences:read` | read | | `get_absence_request` | `absences:read` | read | | `get_absence_type` | `absences:read` | read | | `list_absence_policy_assignments` | `absences:read` | read | | `search_absence_balances` | `absences:read` | read | | `search_absence_policies` | `absences:read` | read | | `search_absence_requests` | `absences:read` | read | | `search_absence_types` | `absences:read` | read | | `archive_absence_policy` | `absences:write` ¹ | write | | `archive_absence_type` | `absences:write` ¹ | write | | `assign_absence_policy` | `absences:write` ¹ | write | | `configure_absence_policy_carryover` | `absences:write` ¹ | write | | `create_absence_policy` | `absences:write` ¹ | write | | `create_absence_request` | `absences:write` ¹ | write | | `create_absence_type` | `absences:write` ¹ | write | | `unarchive_absence_policy` | `absences:write` ¹ | write | | `unarchive_absence_type` | `absences:write` ¹ | write | | `unassign_absence_policy` | `absences:write` ¹ | write | | `update_absence_policy` | `absences:write` ¹ | write | | `update_absence_type` | `absences:write` ¹ | write | | `approve_absence_request` | `absences:transition` ¹ | write | | `cancel_absence_request` | `absences:transition` ¹ | write | | `reject_absence_request` | `absences:transition` ¹ | write | ### Presència [#presence] Presència: el tauler de presència en viu i l'estat de presència per empleat i diari. Requereix el mòdul `control_horario`. *(3 tools)* | Eina | Scope | Categoria | | ----------------------- | --------------- | --------- | | `get_employee_presence` | `presence:read` | read | | `get_live_presence` | `presence:read` | read | | `list_daily_presence` | `presence:read` | read | ### Festius [#holiday] Festius: consultar el calendari de festius de l'empresa i resoldre els festius aplicables a la regió (CCAA) d'un empleat. Requereix el mòdul `control_horario`. *(3 tools)* | Eina | Scope | Categoria | | ----------------------------- | --------------- | --------- | | `get_holiday` | `holidays:read` | read | | `list_holidays` | `holidays:read` | read | | `resolve_applicable_holidays` | `holidays:read` | read | --- # Integració GoCardless (/ca/payments/gocardless) **GoCardless encara no està disponible.** Els seus endpoints v1 i les seves tools MCP **no estan registrats**, així que cridar-los avui retorna `404 route_not_found`. Aquesta pàgina documenta l'**estat** de la integració i la superfície que apareixerà quan s'alliberi — no és una guia d'ús, i res del que segueix s'ha de llegir com «això ja es pot cridar». GoCardless cobra per **domiciliació directa SEPA**: en comptes de carregar una targeta, el teu client signa un **mandat** que t'autoritza a treure diners del seu compte bancari, i tot cobrament posterior corre contra aquest mandat. Aquest model canvia dues coses respecte d'una passarel·la de targeta — els diners es mouen amb un calendari diferit i una finestra de garantia, i el mandat té vida pròpia: neix, s'activa i es pot cancel·lar o caducar amb independència de qualsevol cobrament concret. ## Què vol dir «encara no alliberada» [#status] L'única font de veritat és la llista de passarel·les de pagament alliberades del backend (`integrations.released_providers`), que avui conté **només Stripe**. És configuració, no codi, així que una passarel·la s'encén sense desplegar codi. Mentre GoCardless sigui fora d'aquesta llista: * El seu **bloc de rutes v1 no està registrat**. `GET /v1/gocardless/mandates` i els endpoints `/v1/gocardless-autoinvoicing/*` no existeixen — no són al registre de rutes, ni a l'especificació OpenAPI, ni a la referència d'API d'aquest lloc. * Les seves **tools MCP queden filtrades** del servidor públic, així que un agent ni les descobreix ni les pot cridar. * La passarel·la apareix com a **«Properament»** al marketplace d'integracions del Dashboard, i el flux de connexió està bloquejat també al command handler — fins i tot per a un super-admin que se salti el middleware de mòduls. * L'endpoint de comptes connectats agnòstic de passarel·la filtra els seus resultats a les passarel·les alliberades, així que cap compte de GoCardless no pot aparèixer tampoc per aquí. No falta res ni hi ha res a mig construir: les classes estan **adormides, no absents**. L'alliberament canvia una llista. ## Què existeix ja darrere del flag [#built] | Peça | Estat | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flux de connexió OAuth 2 | Construït. GoCardless s'autentica amb OAuth 2, a diferència de MONEI | | Verificació de la firma del webhook | Construïda | | Normalitzador d'esdeveniments | Construït — mapeja els esdeveniments de GoCardless sobre els mateixos esdeveniments de pagament interns que fa servir el pipeline de Stripe | | Mandats SEPA | Construïts — es guarden amb el seu propi cicle de vida: `pending`, `active`, `cancelled`, `expired`, `failed`, sincronitzat des dels webhooks `mandates.*` | | Comptes connectats per passarel·la | Construïts — llistar, obtenir, actualitzar i desconnectar, replicant el model multi-botiga de Stripe | | Cobraments i rectificatives auto-facturats | Construïts — mateixes regles de decisió, mateixa alta a VeriFactu que a Stripe | ### Quins esdeveniments facturen, i quins no ho fan a propòsit [#events] El normalitzador és més estricte que «qualsevol esdeveniment de pagament emet factura», i la raó és la finestra de garantia SEPA: | Esdeveniment de GoCardless | Què produeix | | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payments.confirmed` | Es tracta com a **cobrat** → corre el flux d'auto-facturació | | `payments.charged_back`, `payments.late_failure` | Es tracten com a **devolució** → flux de factura rectificativa | | `payments.created`, `payments.submitted` | **S'ignoren a propòsit** — són estats intermedis d'un càrrec diferit; facturar abans que el cobrament estigui garantit seria facturar diners que encara poden tornar enrere | | `payments.paid_out` | Avui no té efecte (la conciliació de payouts de GoCardless és un seguiment a part) | | Qualsevol altre | Es registra com a esdeveniment desconegut | Per això un cobrament de GoCardless no es converteix en factura a l'instant en què s'envia, i és la principal diferència de comportament que notaràs si véns de Stripe. ## La superfície que apareix en alliberar-se [#future-surface] ### Endpoints v1 [#future-endpoints] | Endpoint | Scope | | ------------------------------------------------------------------ | -------------------------------- | | `GET /v1/gocardless/mandates` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/connected-accounts` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:read` | | `PUT /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:write` | | `DELETE /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:write` | | `GET /v1/gocardless-autoinvoicing/payments` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/correctives` | `gocardless_autoinvoicing:read` | Els mandats són de **només lectura a l'API pública**: el seu cicle de vida el governen els webhooks `mandates.*`, no les teves crides. ### Tools MCP [#future-tools] `list_gocardless_mandates`, `list_gocardless_connected_accounts`, `get_gocardless_connected_account`, `update_gocardless_connected_account`, `disconnect_gocardless_connected_account`, `list_gocardless_autoinvoiced_payments` i `list_gocardless_autoinvoiced_correctives` — una per cada endpoint de dalt, amb els mateixos scopes. ### Requisit de pla [#plan] La integració amb GoCardless és un mòdul dels plans **Empresario** i **Enterprise**, igual que les integracions amb Stripe i MONEI. Ser al pla correcte no bastarà per si sol mentre la passarel·la segueixi sense alliberar-se — s'han de complir les dues condicions. ## Equivalències amb el flux de Stripe [#stripe-parity] Tot el que ja saps de [Auto-facturació amb Stripe](/payments/stripe-autoinvoicing) es trasllada, perquè la part específica de cada passarel·la acaba al normalitzador: a partir d'aquí, totes dues passarel·les comparteixen el mateix pipeline de facturació, les mateixes decisions fiscals i la mateixa alta a VeriFactu. | Concepte | Stripe | GoCardless | | -------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------- | | Autenticació | OAuth 2 (Stripe Connect) | OAuth 2 | | Multi-botiga | `connected-accounts` per compte | Mateix model, sota `gocardless-autoinvoicing/connected-accounts` | | Senyal de «cobrament amb èxit» | `charge.succeeded` / `invoice.paid` | `payments.confirmed` (passada la finestra de garantia SEPA) | | Devolucions | `charge.refunded` → factura rectificativa | `payments.charged_back` / `payments.late_failure` → factura rectificativa | | Mandats | No aplica | Recurs de primer nivell amb el seu propi cicle de vida | | Factura ordinària o simplificada | Mateixes regles de decisió | Mateixes regles de decisió | | Cicles de subscripció | `invoice.paid` amb un `billing_reason` de subscripció | Sense branca equivalent: el normalitzador només mapeja esdeveniments `payments.*` | | Conciliació de payouts | [Suportada](/payments/payouts-reconciliation) | Avui no coberta | ## Què funciona avui de totes maneres [#inbox] La [safata d'esdeveniments d'integració](/payments/integration-events-inbox) és **agnòstica de la passarel·la** i està registrada sense condicions. Registra esdeveniments de qualsevol integració que escrigui historial, incloses les passarel·les que encara no estan alliberades — perquè amagar aquestes files et deixaria sense explicació per a cobraments que no es van facturar mai. `provider=gocardless` hi és un valor de filtre vàlid des del primer dia. --- # Safata d'esdeveniments d'integració (/ca/payments/integration-events-inbox) Una passarel·la de pagament envia a Factuarea un esdeveniment per tot el que passa al teu compte: un cobrament amb èxit, una devolució emesa, un cicle de subscripció cobrat, un payout que arriba. La majoria d'aquests esdeveniments produeixen alguna cosa — una factura, una rectificativa, un registre de pagament. Alguns no produeixen res, i quan això passa la pregunta interessant sempre és la mateixa: **per què aquest cobrament no va acabar en factura?** La **safata d'esdeveniments d'integració** la respon. Cada esdeveniment que Factuarea rep queda registrat amb allò que va produir i, quan no va produir res, amb un **motiu de descart tipat** sortit d'un catàleg tancat. Sense endevinar als logs, sense obrir un tiquet de suport: el motiu és un valor pel qual pots filtrar i, en els motius sobre els quals pots actuar, ve amb el pas següent i, de vegades, amb la possibilitat de reprocessar l'esdeveniment. La safata és **agnòstica de la passarel·la**. Registra esdeveniments de qualsevol integració que escrigui historial — incloses les passarel·les que encara no estan alliberades i els esdeveniments històrics d'una que es retiri — perquè amagar aquestes files et deixaria sense explicació per a cobraments que no es van facturar mai. L'exposen tres endpoints: | Operació | Endpoint | Scope | | ----------------------------------- | --------------------------------------------- | -------------------------- | | Llistar esdeveniments | `GET /v1/integrations/events` | `integration_events:read` | | Obtenir un esdeveniment | `GET /v1/integrations/events/{event}` | `integration_events:read` | | Reprocessar un esdeveniment aparcat | `POST /v1/integrations/events/{event}/replay` | `integration_events:write` | La mateixa superfície existeix com a tools MCP — `list_integrations_events`, `get_integrations_event` i `replay_integrations_event` — amb els mateixos scopes. ## Recórrer la safata [#listing] Del més recent al més antic, acotat a l'empresa autenticada. Paginació per cursor amb `limit` (d'1 a 100; 25 per defecte) i `starting_after`: ```bash curl -G https://api.factuarea.com/v1/integrations/events \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "provider=stripe" \ --data-urlencode "status=skipped" \ --data-urlencode "limit=50" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f", "object": "integration_event", "provider": "stripe", "event_type": "invoice.paid", "direction": "inbound", "status": "skipped", "discard_reason": "subscription_autoinvoicing_disabled", "discard_reason_label": "Auto-facturación de suscripciones desactivada", "is_actionable": true, "is_replayable": true, "error_message": null, "duration_ms": 412, "created_at": "2026-07-14T09:31:07Z" } ], "has_more": true, "next_cursor": "84120" } ``` Tracta `next_cursor` com a **opac**: en aquest llistat és una cadena numèrica, no un UUID v7 com els cursors dels llistats de documents. Torna'l tal qual a `starting_after`. `discard_reason_label` arriba **sempre en castellà**, l'idioma de la interfície del producte, sigui quin sigui l'idioma de la teva integració. Si construeixes un panell en un altre idioma, basa els teus propis textos en `discard_reason` — aquest valor és l'identificador estable i tancat. ### Filtres [#filters] | Filtre | Valors | Notes | | ------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------- | | `provider` | `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl` | Conjunt tancat | | `status` | `success`, `skipped`, `failure` | Conjunt tancat | | `event_type` | text lliure, coincidència exacta, fins a 100 caràcters | **No** és un enum — vegeu més avall | | `discard_reason` | un dels vint motius del catàleg | Conjunt tancat | | `is_parked` | `true` / `false` | Vegeu la nota de més avall | | `created_at[gte]`, `created_at[lte]` | ISO 8601 | Finestra inclusiva | **`discard_reason` és l'eix tancat; `event_type` no és un enum.** La columna `event_type` barreja a propòsit dues convencions: les branques instrumentades més tard guarden el tipus cru de la passarel·la (`charge.refunded`), mentre que les preexistents conserven el seu propi valor semàntic (`autoinvoice.*`). Busca'l per coincidència exacta quan sàpigues què persegueixes, però no el modelis mai com un conjunt tancat — estaries modelant una cosa que la columna no garanteix. **`is_parked=false` no és el mateix que ometre el paràmetre.** El primer exclou els esdeveniments aparcats; el segon no exclou res. Un valor fora del seu catàleg retorna **422**, i un paràmetre de consulta desconegut retorna **400 `parameter_unknown`** en comptes d'ignorar-se en silenci — un filtre que cau sense avisar et lliura una pàgina que creus acotada i no ho està. ## El catàleg de motius de descart [#reasons] Vint motius tipats, un per cada branca de descart del pipeline de webhooks de les passarel·les. Cadascun declara dues decisions de negoci que **no** són banderes decoratives: * **Accionable** — pot fer-hi alguna cosa el titular del compte? Només els motius accionables avisen. Avisar algú d'un descart que no pot resoldre li ensenya a ignorar la safata, i així és com es perd l'avís que sí que importava. * **Aparcat** — reprocessar el mateix contingut podria donar un altre resultat? Només els esdeveniments aparcats guarden el seu contingut xifrat i admeten un reprocessament. La regla que hi ha darrere de la columna d'aparcament: un esdeveniment s'aparca quan el descart el va causar un **estat extern que pots canviar** (un ajust apagat, un compte connectat que es va desvincular, una moneda encara sense tipus de canvi). No s'aparca quan la causa és el **contingut del mateix esdeveniment** (mal format, duplicat, de tipus no cobert, import zero, cicle ja facturat) — reprocessar-lo prendria exactament la mateixa branca i només escriuria una segona fila. D'aquí l'invariant: **tot motiu aparcat és accionable**, i sis dels nou accionables s'aparquen. | Motiu | Què el provoca | Accionable | Aparcat | Què fer | | ------------------------------------- | -------------------------------------------------------------------------------------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------------ | | `event_not_normalizable` | Esdeveniment mal format, o d'un tipus que no es pot interpretar | No | No | Res — no pots arreglar el payload de la passarel·la | | `duplicate_redelivery` | L'esdeveniment ja es va processar; el seu efecte existeix | No | No | Res — reprocessar-lo seria un no-op per deduplicació | | `connected_account_missing` | El webhook està mal configurat a la passarel·la: l'esdeveniment no diu a quin compte pertany | **Sí** | No | Revisa a la passarel·la que el webhook s'envia des del compte que tens vinculat a Factuarea | | `connected_account_unknown` | El compte existeix a la passarel·la però no està vinculat a Factuarea | **Sí** | **Sí** | Torna a vincular aquest compte de la passarel·la i reprocessa l'esdeveniment | | `spontaneous_payment_missing_id` | El cobrament no porta id, així que no hi ha clau d'idempotència | No | No | Res — reprocessar-lo duplicaria o tornaria a fallar | | `autoinvoicing_disabled` | L'auto-facturació està desactivada per a aquesta integració | **Sí** | **Sí** | Activa l'auto-facturació i reprocessa, **o** crea la factura a mà — mai les dues coses | | `unsupported_currency` | El tipus de canvi del Banc Central Europeu del dia encara no està disponible | **Sí** | **Sí** | Reprocessa l'esdeveniment més tard, quan el tipus oficial del dia estigui publicat | | `refund_without_items` | La devolució no porta reemborsaments individuals que rectificar | No | No | Res — no hi ha res a emetre | | `refund_autoinvoicing_disabled` | La rectificativa automàtica està desactivada per a aquesta integració | **Sí** | **Sí** | Activa la rectificativa automàtica i reprocessa, **o** emet la rectificativa a mà — mai les dues coses | | `subscription_missing_invoice_id` | El cicle cobrat no té identificador de factura | **Sí** | No | Crea a mà la factura d'aquest cicle; reprocessar donaria el mateix resultat | | `subscription_proration_review` | S'ha cobrat un prorrateig solt i exigeix una decisió humana | **Sí** | No | Comprova l'import del prorrateig a la passarel·la i emet la factura a mà | | `subscription_not_a_cycle` | La factura de la passarel·la no correspon a un cicle de subscripció facturable | No | No | Res — el descart és correcte | | `subscription_trial_skipped` | Import zero o negatiu (prova o crèdit): no hi ha base imposable | No | No | Res — no hi ha res a facturar | | `subscription_autoinvoicing_disabled` | L'auto-facturació de subscripcions està desactivada | **Sí** | **Sí** | Activa l'auto-facturació de subscripcions i reprocessa l'esdeveniment | | `subscription_already_invoiced` | El cicle ja té la seva factura | No | No | Res — reprocessar-lo seria un no-op per idempotència | | `payout_missing_id` | El payout no porta identificador | No | No | Res — no es pot conciliar ni reprocessar amb seguretat | | `payout_connected_account_missing` | El compte connectat del payout no està vinculat | **Sí** | **Sí** | Vincula el compte connectat i reprocessa l'esdeveniment | | `payment_failed` | El cobrament ha fallat a la passarel·la | No | No | Res — no hi ha res a emetre ni a reintentar | | `event_type_not_covered` | Tipus d'esdeveniment fora de l'abast del producte | No | No | Res — reprocessar-lo tornaria a no fer res | | `checkout_lines_retrieve_failed` | Degradació, no descart: la factura **sí** que es va emetre, amb una única línia | No | No | Res a reprocessar; revisa les línies de la factura si t'importa el desglossament | Un motiu sense res a fer ho diu explícitament. Onze dels vint són informatius, i el contracte no s'inventa una instrucció per a ells: l'endpoint de detall retorna `recommended_action: null` en comptes d'una frase fabricada per omplir el camp. **«O l'una, o l'altra» vol dir una, no les dues.** Dos motius t'ofereixen dues sortides — activar l'ajust i reprocessar, o emetre el document a mà. Són **excloents**. La idempotència del reprocessament va per la identitat del cobrament i només reconeix els documents emesos per aquesta mateixa via automàtica, així que una factura que hagis creat a mà **no** el frena. Fer les dues coses deixa el mateix cobrament amb **dues factures**, cadascuna numerada a la seva sèrie i donada d'alta a VeriFactu — un dany fiscal que només es desfà amb una factura rectificativa. ## Avisos: només el que pots arreglar [#notifications] Un descart accionable avisa els administradors del compte. Un d'informatiu no avisa mai. L'avís porta throttling: si ja existeix un avís **sense llegir** de la mateixa empresa, la mateixa passarel·la i el mateix motiu dins de les últimes 24 hores, no se'n crea un segon — un webhook mal configurat dispara centenars d'esdeveniments idèntics. La condició és *sense llegir* a propòsit: un cop l'has llegit, si continuen arribant descarts, el següent **sí** que avisa. Això no és soroll, vol dir que la incidència segueix viva. ## L'aparcament i la finestra de 30 dies [#retention] Quan un motiu és aparcable, Factuarea guarda l'esdeveniment cru **xifrat en repòs**, per poder reprocessar-lo més tard. Aquest contingut **no es retorna mai** per l'API — ni al llistat, ni al detall. Conté dades personals dels teus clients finals i dades de pagament, i existeix per a exactament un propòsit: fer possible el reprocessament. El contingut es **purga als 30 dies d'aparcar-se l'esdeveniment**. La fila sobreviu: el seu motiu, el seu estat, la seva data i la seva marca `is_parked` segueixen a la teva safata indefinidament, perquè el registre que un cobrament no va produir factura és historial que pots necessitar molt després que el contingut caduqui. **Un esdeveniment que segueix amb `is_parked: true` però ja no és `is_replayable` vol dir exactament una cosa: la finestra de retenció s'ha esgotat.** La marca es deriva de si el contingut encara hi és, així que canvia sola el dia que corre la purga. Un reprocessament intentat després retorna 422 amb el subcodi `integration_event_payload_purged`. ## El detall: què fer a continuació [#detail] L'endpoint de detall retorna tot el del llistat, més `recommended_action`: una frase en imperatiu amb el pas següent per a aquell motiu concret, o `null` quan el motiu és informatiu. ```bash curl https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` La frase distingeix a propòsit els motius reproduïbles («… i reprocessa l'esdeveniment») dels que no ho són («… emet-la a mà»), de manera que mai no t'apunta a una operació que respondria 422. Un esdeveniment d'una altra empresa i un esdeveniment que no existeix retornen el **mateix** 404 `resource_not_found`. L'endpoint no revela mai si un id existeix en un altre lloc. ## Reprocessar un esdeveniment aparcat [#replay] Torna a processar un esdeveniment de la passarel·la que va quedar aparcat, un cop ja no hi és la causa que li va impedir produir el seu efecte — has tornat a activar l'auto-facturació, has tornat a vincular el compte connectat, ja hi ha disponible el tipus de canvi oficial del dia. **Aquesta acció pot tenir conseqüències fiscals reals.** Si la causa del descart ja està resolta, el reprocessament **pot emetre una factura real**, amb el seu número de sèrie i la seva alta a VeriFactu. No és un reintent innocu: confirma-ho amb el titular del compte abans de cridar-lo. Per això porta el seu propi scope d'escriptura, `integration_events:write`, en comptes del scope de lectura de la safata — una credencial de només lectura no ha de poder facturar mai. ```bash curl -X POST https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f/replay \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Quatre propietats d'aquesta operació importen més que la seva firma: * **No duplica factures.** El reprocessament passa pel mateixíssim control d'idempotència que l'intent original, així que si aquell cobrament ja va produir una factura, el job s'atura sol i no crea res. * **És asíncron.** `202` vol dir acceptat i encuat, **no** completat. El cos retorna l'esdeveniment tal com està *ara* — el seu `is_replayable` segueix sent `true` —, no el resultat del reintent. El resultat apareix com un esdeveniment **nou** a la safata, així que consulta `GET /v1/integrations/events` per veure com ha acabat. * **Si la causa segueix present, l'esdeveniment es descarta un altre cop** i es registra de nou. És correcte, i és observable. * **No admet entrada.** Qualsevol paràmetre de consulta o clau del cos retorna **400 `parameter_unknown`** en comptes d'ignorar-se. Enviar-ne un vol dir que et penses que estàs configurant alguna cosa del reintent — un mode, una sèrie, una data — que aquesta operació no suporta, i acceptar-ho en silenci confirmaria aquella expectativa falsa sobre una acció que pot emetre una factura. Un cos buit, o directament cap cos, és el cas normal. ### Quan es rebutja un reprocessament [#replay-422] `is_replayable: true` és el contracte: quan val `true`, el reprocessament **no** respon 422. És la conjunció de tres condicions — l'esdeveniment està aparcat, encara conserva el seu contingut i el seu motiu admet reprocessament —, avaluades en aquest mateix ordre pel mateix handler que guarda el reprocessament. Això és el que et permet oferir un botó de reintent sense endevinar. Els tres rebutjos retornen **422 `business_rule_violation`** i et diuen quin és a través del `subcode`: | `subcode` | Què significa | Hi ha sortida? | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `integration_event_not_parked` | L'esdeveniment no es va aparcar mai — o va tenir èxit, o el seu motiu no guarda el contingut | No, i mai no n'hi haurà | | `integration_event_payload_purged` | Es va aparcar, però el seu contingut es va esborrar en esgotar-se la finestra de 30 dies | No — resol-ho a mà | | `integration_event_reason_not_replayable` | Està aparcat i conserva el seu contingut, però el seu motiu tornaria a prendre exactament la mateixa branca | No — segueix en el seu lloc l'acció recomanada | ## On encaixa això [#related] * [Auto-facturació amb Stripe](/payments/stripe-autoinvoicing) — el flux que produeix la majoria dels esdeveniments que trobaràs aquí, inclosos els [cicles de subscripció](/payments/stripe-autoinvoicing#subscriptions), l'ajust dels quals és darrere de `subscription_autoinvoicing_disabled`. * [Payouts i conciliació bancària](/payments/payouts-reconciliation) — la ingesta de payouts que hi ha darrere de `payout_missing_id` i `payout_connected_account_missing`. * [Mode de prova i sandbox](/guides/test-mode) — valida el teu tractament de la safata amb una clau `fact_test_` abans de connectar un botó de reprocessament a una credencial de producció. * [Gestió d'errors](/guides/errors) — l'envelope de les respostes 400, 404 i 422 citades més amunt. --- # Conciliar amb la metadata de sistema (/ca/payments/metadata-reconciliation) Tots els documents de Factuarea porten un objecte `metadata` de forma lliure on pots escriure el que necessitis. A les factures que Factuarea emet **automàticament des d'un cicle de subscripció de Stripe**, la plataforma escriu a més un grapat de **claus de sistema** que lliguen la factura al cobrament del qual va néixer: quina factura de Stripe, quina subscripció, quin període de facturació. Aquestes claus són el que fa possible la conciliació sense mantenir la teva pròpia taula de correspondències. Fa temps que s'escriuen; aquesta pàgina és on queden documentades. **Abast: cicles de subscripció.** Aquestes claus les escriu el flux que auto-emet una factura per un **cicle de subscripció cobrat** (vegeu [cicles de subscripció](/payments/stripe-autoinvoicing#subscriptions)). Els cobraments solts auto-facturats des de `charge.succeeded` **no** les porten avui — per a aquests, correlaciona a través del [llistat de cobraments auto-facturats](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list), que exposa els identificadors del costat del cobrament. ## Les claus de sistema [#keys] | Clau | Què identifica | Format | Presència | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | `stripe_invoice_id` | La factura de Stripe del cicle cobrat | Id de Stripe, `in_…` | **Sempre** | | `billing_reason` | Per què Stripe va facturar aquell cicle | El `billing_reason` cru de Stripe — a la pràctica `subscription_create` (primer cicle) o `subscription_cycle` (cada renovació), els dos únics que s'auto-facturen | **Sempre** | | `stripe_subscription_id` | La subscripció a la qual pertany el cicle | Id de Stripe, `sub_…` | Opcional — s'omet quan Stripe no envia id de subscripció | | `period_start` | Primer dia del període facturat | `YYYY-MM-DD`, **UTC** | Opcional — s'omet quan falta el timestamp del període | | `period_end` | Fi del període facturat, literal del `period_end` d'Stripe — és el límit **exclusiu**, així que en un cicle mensual és el primer dia del període següent, no l'últim dia d'aquest | `YYYY-MM-DD`, **UTC** | Opcional — s'omet quan falta el timestamp del període | Les claus opcionals **no es materialitzen com a nul·les ni buides**: quan el valor no aplica, la clau no s'escriu. És deliberat — una clau present amb valor buit semblaria una correlació que existeix però està en blanc, i qualsevol codi que la llegís hauria de distingir «sense subscripció» de «subscripció desconeguda». Comprova la presència de la clau, no el seu valor. **Són claus de sistema. No les escriguis a mà.** Són la correlació entre una factura de Factuarea i un objecte de Stripe, i les receptes de conciliació de més avall hi confien. Escriure tu mateix `stripe_invoice_id` a una factura que no hi ve al cas fa que aquella factura aparegui en una conciliació a la qual no pertany, i res no ho assenyalarà — `metadata` és de forma lliure per disseny. Fes servir les teves pròpies claus (`erp_ref`, `project_code`, …) per a les teves pròpies correlacions. Les claus es llegeixen allà on sigui la factura: `metadata` forma part del recurs de factura, i torna com un objecte JSON (`{}` quan és buit). ## Filtrar per metadata [#filter] Vuit llistats v1 accepten un filtre `metadata`: | Recurs | Endpoint | | ------------------- | ------------------------------------------------------------------------------------------------------- | | Factures | [`GET /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.list) | | Pressupostos | [`GET /v1/quotes`](/api-reference/quotes/public-api.v1.quotes.list) | | Factures proforma | [`GET /v1/proformas`](/api-reference/proformas/public-api.v1.proformas.list) | | Albarans | [`GET /v1/delivery_notes`](/api-reference/delivery-notes/public-api.v1.delivery_notes.list) | | Factures de compra | [`GET /v1/purchase_invoices`](/api-reference/purchase-invoices/public-api.v1.purchase_invoices.list) | | Factures recurrents | [`GET /v1/recurring_invoices`](/api-reference/recurring-invoices/public-api.v1.recurring_invoices.list) | | Productes | [`GET /v1/products`](/api-reference/products/public-api.v1.products.list) | | Proveïdors | [`GET /v1/suppliers`](/api-reference/suppliers/public-api.v1.suppliers.list) | La sintaxi és `deepObject`: `metadata[clau]=valor`, un paràmetre de consulta per parell. * **Els parells es combinen amb AND.** Dos parells retornen els documents que compleixen tots dos. * **Coincidència exacta** al valor; no hi ha coincidència parcial ni per prefix. * **Fins a 50 parells** per petició; a partir d'aquí retorna `parameter_invalid_range`. * **Les claus** han d'encaixar a `[A-Za-z0-9_.-]` i mesurar entre 1 i 64 caràcters; qualsevol altra cosa retorna `parameter_invalid_enum`. * El filtre queda **fora** del contracte `{operator, value}` dels filtres de columna, així que no existeix la forma `metadata[clau][eq]`. `metadata[clau]=valor` és tota la sintaxi. **Deixa que curl codifiqui els claudàtors.** `[` i `]` són caràcters de glob per a curl i caràcters reservats en una URL. Passa els parells amb `-G --data-urlencode`, com a les receptes de més avall, i curl els codifica correctament. Enganxar un `?metadata[clau]=valor` cru en un shell és d'on surt habitualment el «el filtre s'està ignorant». ## Recepta: totes les factures d'una subscripció [#recipe-subscription] La conciliació que necessites quan un client et demana totes les factures del seu pla, o quan tanques l'any d'un subscriptor: ```bash curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \ --data-urlencode "limit=100" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f", "object": "invoice", "number": "2026/0184", "total": "49.90", "currency": "EUR", "metadata": { "stripe_invoice_id": "in_1QRstuVWXYZabcde", "billing_reason": "subscription_cycle", "stripe_subscription_id": "sub_1QRstuVWXYZabcde", "period_start": "2026-07-01", "period_end": "2026-08-01" } } ], "has_more": false, "next_cursor": null } ``` El llistat es pagina per cursor com tots els altres: continua llegint mentre `has_more` valgui `true`, tornant `next_cursor` a `starting_after`. Vegeu [Paginació](/guides/pagination). ## Recepta: les factures d'un període de facturació [#recipe-period] Dos parells, combinats amb AND: la subscripció i el primer dia del període. És la consulta que respon a «s'ha facturat el cicle de juliol?». ```bash curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \ --data-urlencode "metadata[period_start]=2026-07-01" ``` Com que `period_start` i `period_end` són dates exactes en **UTC**, filtra pel límit del període en comptes de per un rang — el valor de la metadata és el dia que Stripe reporta per al cicle, no un mes de calendari local. Per escombrar un mes sencer de cicles de totes les subscripcions, treu el parell de la subscripció i consulta `metadata[period_start]` tot sol. **Filtra per `period_start`, no per `period_end`.** `period_end` és el límit superior exclusiu d'Stripe: el cicle de juliol d'una subscripció mensual porta `period_start: 2026-07-01` i `period_end: 2026-08-01`. Consultar `metadata[period_end]=2026-07-31` no retorna res, i aquest resultat buit s'assembla exactament a un cicle que no es va facturar mai. Un array `data` buit per a un període que esperaves facturat és un senyal real, no un error del filtre. És exactament el cas que explica la [safata d'esdeveniments d'integració](/payments/integration-events-inbox): obre-la filtrada per `provider=stripe` i `status=skipped` i el motiu de descart tipat et dirà si el cicle es va saltar perquè l'auto-facturació estava apagada, perquè el cicle no portava import, o per una altra cosa — i si el pots reprocessar. ## Relacionat [#related] * [Auto-facturació amb Stripe](/payments/stripe-autoinvoicing) — com s'emeten, per començar, les factures que aquestes claus descriuen. * [Safata d'esdeveniments d'integració](/payments/integration-events-inbox) — per què un cicle que esperaves no va produir mai factura. * [Etiquetes i camps personalitzats](/guides/tags-and-custom-fields) — escriure i consultar les teves **pròpies** claus de metadata. --- # Integració MONEI (/ca/payments/monei) **MONEI encara no està disponible.** Els seus endpoints v1 i les seves tools MCP **no estan registrats**, així que cridar-los avui retorna `404 route_not_found`. Aquesta pàgina documenta l'**estat** de la integració i la superfície que apareixerà quan s'alliberi — no és una guia d'ús. MONEI és una passarel·la de pagament espanyola que cobra amb **targeta i Bizum**. Els diners es mouen en el moment de la captura, com en una passarel·la de targeta i a diferència de la domiciliació directa SEPA — que és la raó que la seva integració tingui una forma una mica diferent de la de [GoCardless](/payments/gocardless). ## Què vol dir «encara no alliberada» [#status] L'única font de veritat és la llista de passarel·les de pagament alliberades del backend (`integrations.released_providers`), que avui conté **només Stripe**. És configuració, no codi, així que una passarel·la s'encén sense desplegar codi. Mentre MONEI sigui fora d'aquesta llista: * El seu **bloc de rutes v1 no està registrat**. Els endpoints `/v1/monei-autoinvoicing/*` no existeixen — no són al registre de rutes, ni a l'especificació OpenAPI, ni a la referència d'API d'aquest lloc. * Les seves **tools MCP queden filtrades** del servidor públic. * La passarel·la apareix com a **«Properament»** al marketplace d'integracions del Dashboard, i el flux de connexió està bloquejat també al command handler. * L'endpoint de comptes connectats agnòstic de passarel·la filtra els seus resultats a les passarel·les alliberades, així que cap compte de MONEI no apareix tampoc per aquí. Les classes estan **adormides, no absents**. L'alliberament canvia una llista. ## Sense recurs de mandats, i no és una omissió [#no-mandates] GoCardless cobra per domiciliació directa SEPA, així que un **mandat** — l'autorització permanent del client per treure diners del seu compte bancari — és un objecte de primer nivell amb el seu propi cicle de vida, i té el seu propi endpoint i la seva pròpia tool MCP. **MONEI no fa servir domiciliació directa SEPA.** No hi ha cap autorització permanent que modelar, així que no hi ha recurs `mandates`, ni estats de mandat que sincronitzar, ni webhooks de mandats. Si estàs portant una integració escrita contra GoCardless, aquella branca sencera desapareix; no hi ha res sobre què mapejar-la. ## Què existeix ja darrere del flag [#built] | Peça | Estat | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | Flux de connexió | Construït. MONEI s'autentica amb una **API key**, no amb OAuth 2 | | Verificació de la firma del webhook | Construïda | | Normalitzador d'esdeveniments | Construït — mapeja els estats de pagament de MONEI sobre els mateixos esdeveniments de pagament interns que fa servir el pipeline de Stripe | | Comptes connectats per passarel·la | Construïts — llistar, obtenir, actualitzar i desconnectar, replicant el model multi-botiga de Stripe | | Cobraments i rectificatives auto-facturats | Construïts — mateixes regles de decisió, mateixa alta a VeriFactu que a Stripe | | Mandats | **No aplica** — vegeu més amunt | ### Quins estats facturen, i quins no ho fan a propòsit [#events] MONEI informa de l'estat d'un pagament com un `status` del mateix objecte de pagament, i el normalitzador s'hi basa: | Estat de MONEI | Què produeix | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SUCCEEDED` | Es tracta com a **cobrat** → corre el flux d'auto-facturació | | `REFUNDED`, `PARTIALLY_REFUNDED` | Es tracten com a **devolució** → flux de factura rectificativa, total o parcial | | `FAILED`, `CANCELED` | Es registren com a cobrament fallit; no s'emet res | | `AUTHORIZED` | **S'ignora a propòsit** — una autorització sense captura no són diners cobrats, i facturar-la seria facturar un cobrament que potser no es capturarà mai | | Qualsevol altre | Es registra com a esdeveniment desconegut | Un pagament sense identificador es descarta abans que cap altra cosa: sense id no hi ha identitat canònica ni clau d'idempotència, així que no es podria deduplicar ni reprocessar amb seguretat. ## La superfície que apareix en alliberar-se [#future-surface] ### Endpoints v1 [#future-endpoints] | Endpoint | Scope | | ------------------------------------------------------------- | --------------------------- | | `GET /v1/monei-autoinvoicing/connected-accounts` | `monei_autoinvoicing:read` | | `GET /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:read` | | `PUT /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:write` | | `DELETE /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:write` | | `GET /v1/monei-autoinvoicing/payments` | `monei_autoinvoicing:read` | | `GET /v1/monei-autoinvoicing/correctives` | `monei_autoinvoicing:read` | El llistat de cobraments porta un filtre `origin` (`subscription` / `oneshot`) per simetria amb les altres passarel·les. Si MONEI no té cobraments de subscripció teus, `origin=subscription` retorna una pàgina buida en comptes d'un error. ### Tools MCP [#future-tools] `list_monei_connected_accounts`, `get_monei_connected_account`, `update_monei_connected_account`, `disconnect_monei_connected_account`, `list_monei_autoinvoiced_payments` i `list_monei_autoinvoiced_correctives` — una per cada endpoint de dalt, amb els mateixos scopes. ### Requisit de pla [#plan] La integració amb MONEI és un mòdul dels plans **Empresario** i **Enterprise**, igual que les integracions amb Stripe i GoCardless. Ser al pla correcte no bastarà per si sol mentre la passarel·la segueixi sense alliberar-se — s'han de complir les dues condicions. ## Equivalències amb el flux de Stripe [#stripe-parity] La part específica de cada passarel·la acaba al normalitzador: a partir d'aquí, totes les passarel·les comparteixen el mateix pipeline de facturació, les mateixes decisions fiscals i la mateixa alta a VeriFactu que descriu [Auto-facturació amb Stripe](/payments/stripe-autoinvoicing). | Concepte | Stripe | MONEI | | ------------------------------ | ----------------------------------------------------- | ----------------------------------------------------------- | | Autenticació | OAuth 2 (Stripe Connect) | API key | | Multi-botiga | `connected-accounts` per compte | Mateix model, sota `monei-autoinvoicing/connected-accounts` | | Senyal de «cobrament amb èxit» | `charge.succeeded` / `invoice.paid` | Estat de pagament `SUCCEEDED` | | Devolucions | `charge.refunded` → factura rectificativa | `REFUNDED` / `PARTIALLY_REFUNDED` → factura rectificativa | | Autorització sense capturar | No es factura | `AUTHORIZED`, no es factura | | Mandats | No aplica | No aplica | | Cicles de subscripció | `invoice.paid` amb un `billing_reason` de subscripció | Sense branca equivalent al normalitzador | | Conciliació de payouts | [Suportada](/payments/payouts-reconciliation) | Avui no coberta | ## Què funciona avui de totes maneres [#inbox] La [safata d'esdeveniments d'integració](/payments/integration-events-inbox) és **agnòstica de la passarel·la** i està registrada sense condicions, així que `provider=monei` hi és un valor de filtre vàlid des del primer dia — inclosos els esdeveniments històrics, que és justament la raó que aquestes files no s'amaguin. --- # Payouts i conciliació bancària (/ca/payments/payouts-reconciliation) Quan connectes Stripe via **Stripe Connect**, Stripe no transfereix cada cobrament al teu banc d'un en un: agrupa molts cobraments, resta les seves comissions i envia un únic **payout** (`po_xxx`) al teu compte. La línia que apareix al teu extracte diu `STRIPE PAYOUT 1.234,56 €` i és el **net** de *N* cobraments menys comissions, així que mai casa amb el total d'una sola factura. Factuarea tanca aquest cicle. **Ingereix cada payout**, el vincula amb els cobraments que el componen i concilia la línia bancària contra el payout, no contra una factura. Quan confirmes el match, el payout, la transacció bancària i tots els cobraments subjacents queden marcats com a conciliats en un únic pas atòmic. Els payouts són de **només lectura a l'API pública**: pots llistar-los i inspeccionar-los juntament amb el seu estat de conciliació, però la conciliació en si es fa al Dashboard contra el teu extracte Norma 43 importat. Dos endpoints v1 els exposen: * [Llistar payouts de Stripe](/api-reference/stripe/public-api.v1.payouts.list) (`payouts:read`). * [Obtenir un payout de Stripe](/api-reference/stripe/public-api.v1.payouts.show) (`payouts:read`). ## Ingesta d'un payout [#ingestion] Cada vegada que Stripe completa un payout envia un webhook **`payout.paid`** al teu endpoint de Connect. Factuarea hi reacciona: 1. Registra el payout — `connected_account_id` (`acct_xxx`), `stripe_payout_id` (`po_xxx`), els imports **net**, **comissions** i **brut**, la divisa i la **data d'arribada** prevista — amb `status: ingested`. 2. Llegeix les **balance transactions** del payout en el teu nom (una crida de només lectura i paginada a Stripe) per descobrir **quins cobraments** agrupa el payout i el total de comissions. Aquest desglossament es guarda com la `composition` informativa. La ingesta és **idempotent en dos nivells**: per l'`event.id` de Stripe (un `payout.paid` reentregat es processa com a màxim una vegada) i pel `stripe_payout_id` (dos esdeveniments diferents del mateix `po_xxx` mai creen una fila duplicada — la unicitat està garantida a la base de dades, fins i tot amb webhooks concurrents). Si el desglossament no es pot llegir (un error transitori de Stripe després dels reintents), el payout **no** queda ingerit a mitges: es reintenta el pas complet, i l'`event.id` només es marca com a processat quan la ingesta acaba amb èxit. Mai hi ha una fila de payout sense els seus imports. Per ingerir payouts has d'habilitar l'esdeveniment **`payout.paid`** al teu endpoint de webhook de Connect al Stripe Dashboard. Com sempre, valida el flux primer amb una clau `fact_test_` — al [sandbox](/guides/test-mode) la ingesta corre contra una empresa aïllada amb tots els efectes externs desactivats. ## Vinculació dels cobraments amb el payout [#linking] Les balance transactions diuen a Factuarea quins cobraments componen el payout. Cada component porta el seu `payment_intent` (`pi_xxx`) de Stripe — el **mateix identificador** que Factuarea va estampar al `Payment` que va registrar quan el cobrament es va auto-facturar (vegeu [Auto-facturació Stripe](/payments/stripe-autoinvoicing)). Amb aquest identificador, Factuarea troba els `Payment` corresponents de la teva empresa i estampa l'id del payout a cadascun. Així els cobraments d'un payout queden vinculats als cobraments que els van produir — la relació que més tard permet que la conciliació baixi en cascada fins a cada cobrament. La vinculació és **best-effort i idempotent**: tornar a vincular el mateix payout no és un error, i un component sense `Payment` corresponent (un cobrament rebut abans que existís l'auto-facturació, o per una altra eina) es registra a la [safata d'esdeveniments d'integració](/payments/integration-events-inbox) sense bloquejar la resta. Un payout que arriba abans que vinculis el seu compte connectat queda aparcat allà com a `payout_connected_account_missing`: vincula el compte i reprocessa l'esdeveniment, i el payout s'ingereix. El payout s'ingereix igualment — la conciliació casa pel **import net**, mai exigeix un desglossament complet dels cobraments. ## Conciliació contra l'extracte bancari [#reconciliation] La conciliació corre sobre el teu extracte bancari **Norma 43** importat, al Dashboard. Quan puges un extracte, Factuarea proposa matches per a cada línia d'abonament. **Abans** d'intentar casar un abonament contra una factura pendent, comprova si la línia és un **payout**: | Senyal | Regla | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Import** | L'abonament bancari equival a l'import **net** del payout (dins d'una petita tolerància d'arrodoniment). Aquesta és la senyal dura. | | **Finestra d'arribada** | La data valor del banc cau dins de **±3 dies** de l'`arrival_date` del payout (els bancs liquiden amb un petit desfasament). | | **Descripció** | Una menció `STRIPE` **suma confiança** però mai és requisit — la redacció varia entre bancs. | El resultat depèn de quants payouts `ingested` encaixin: * **Exactament un** → un match de payout **automàtic**. * **Més d'un** → un **suggeriment** amb els candidats, perquè triïs. * **Cap** → la línia continua al matching ordinari **per factura** (un abonament que no és un payout ha de seguir casant amb una factura). Una transacció casada contra un payout queda **exclosa** del matching per factura, i viceversa, de manera que la mateixa línia bancària mai es concilia dues vegades. El matching és **per divisa**. El payout s'ingereix en la seva divisa real i només casa amb línies bancàries en la **mateixa** divisa — no hi ha conversió (això és conciliació comptable, no una operació fiscal, així que mai emet ni altera una factura). ## Confirmació del match [#confirm] Quan confirmes un match de payout al Dashboard, Factuarea executa **una única transacció atòmica**: 1. La transacció bancària es marca **conciliada** (amb tipus de match `payout`). 2. El payout transiciona a **`reconciled`** (un estat terminal) i registra la referència de la transacció bancària a `bank_transaction_ref`. 3. Cada `Payment` vinculat al payout queda estampat amb el seu `reconciled_at` i la referència de la transacció bancària. Hi ha guards que protegeixen cada pas: la transacció bancària ha de seguir `pending`, el payout ha de seguir `ingested`, i els imports han de coincidir. Confirmar un payout que **ja està conciliat**, o una transacció que **ja està casada**, es rebutja sense deixar cap estat a mitges. Tota l'operació és tenant-scoped: un payout o una transacció d'una altra empresa mai és visible ni conciliable. ## Inspeccionar payouts a l'API [#api] Llista els payouts de la teva empresa amb paginació per cursor, filtrats per `status` de conciliació i per finestra de data d'arribada: ```bash curl "https://api.factuarea.com/v1/payouts?status=ingested&arrival_date[gte]=2026-01-01&limit=25" \ -H "Authorization: Bearer fact_test_…" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7e10-9c1a-1f2e3d4c5b6a", "object": "stripe_payout", "connected_account_id": "acct_1QabcDEF2ghIJklm", "stripe_payout_id": "po_1QabcDEF2ghIJklm", "amount_net": "1234.56", "fee_total": "37.04", "amount_gross": "1271.60", "currency": "EUR", "arrival_date": "2026-01-08", "status": "ingested", "reconciled_at": null, "bank_transaction_ref": null, "composition": { "components": [ { "payment_intent": "pi_3QabcDEF2ghIJklm", "charge_id": "ch_3QabcDEF2ghIJklm", "amount": 121.00, "fee": 3.50 } ], "fee_total": 37.04 } } ], "has_more": false, "next_cursor": null } ``` Cada identificador és **opac**: * `id` és l'**UUID v7** del payout — la identitat pública del recurs. * `connected_account_id` (`acct_xxx`) i `stripe_payout_id` (`po_xxx`) són **ids externs de Stripe**, no foreign keys a altres recursos de Factuarea. * `bank_transaction_ref` és l'UUID (v7) de la transacció de l'extracte bancari conciliada — `null` mentre el payout segueix `ingested`. * `composition` referencia **ids opacs de Stripe** (`payment_intent` = `pi_xxx`, `charge_id` = `ch_xxx`), no UUIDs interns de cobrament. Pot estar buit quan no s'ha pogut llegir el desglossament. L'`status` d'un payout és `ingested` fins que es concilia contra una línia bancària, i després `reconciled` (terminal). Obtén un payout concret pel seu `id`: ```bash curl "https://api.factuarea.com/v1/payouts/0192f3a4-7b2c-7e10-9c1a-1f2e3d4c5b6a" \ -H "Authorization: Bearer fact_test_…" ``` Retorna `404` si el payout no existeix o pertany a una altra empresa. ## L'esdeveniment sortint [#event] Quan un payout es concilia, Factuarea emet l'esdeveniment **`payout.reconciled`**. El seu payload porta el snapshot complet del payout (`object`) més l'import net, la divisa i la referència de la transacció bancària, de manera que un receptor de webhooks pugui tancar la seva pròpia comptabilitat en el moment en què els diners es confirmen al banc. Subscriu-t'hi com a qualsevol altre esdeveniment — vegeu [Webhooks](/guides/webhooks) i [Esdeveniments](/guides/events). No existeix un scope `payouts:write`: els payouts s'observen, mai es muten, a través de l'API. L'únic canvi d'estat — la conciliació — es dispara des del Dashboard contra el teu extracte Norma 43, i l'esdeveniment és el que notifica a la teva integració. --- # Auto-facturació amb Stripe (/ca/payments/stripe-autoinvoicing) Quan connectes Stripe mitjançant **Stripe Connect**, Factuarea pot **emetre una factura automàticament per cada cobrament correcte**: el cobrament es converteix en una factura amb `status: sent`, es dona d'alta a VeriFactu i se li registra un `Payment`. El flux és idempotent d'extrem a extrem, així que un webhook reentregat mai produeix una factura duplicada. L'alimenten dos fluxos: * **Flux A** — un cobrament que paga una factura de Factuarea ja existent (una Checkout Session que Factuarea va crear des d'un enllaç de pagament). La factura ja existeix; el cobrament la marca com a pagada. * **Flux B** — un cobrament espontani sense factura prèvia (un Payment Link que el comerç va crear al seu propi Dashboard de Stripe, o qualsevol altre cobrament de Connect). Factuarea crea la factura a partir del cobrament. La configuració es llegeix i s'escriu a través dels endpoints v1 a nivell d'empresa: * [Obtenir la configuració d'auto-facturació](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show) (`stripe_autoinvoicing:read`). * [Actualitzar la configuració d'auto-facturació](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.update) (`stripe_autoinvoicing:write`). * [Llistar cobraments auto-facturats](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list) (`stripe_autoinvoicing:read`). * [Llistar rectificatives auto-facturades](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list) (`stripe_autoinvoicing:read`). Si gestiones diverses **botigues** amb comptes de Stripe diferents, cada compte té la seva pròpia sèrie i la seva pròpia configuració — consulta [Diverses botigues](#multi-store). L'auto-facturació està limitada pel mòdul d'**integració amb Stripe** del teu pla i ve **desactivada per defecte** — activa-la explícitament amb `enabled: true`. **Què exposa l'API de Stripe.** **Configuració** d'auto-facturació a nivell d'empresa (una comoditat heretada del model de botiga única — la font de veritat real és la configuració per compte connectat, veure [Diverses botigues](#multi-store)), **comptes connectats**, **cobraments auto-facturats**, **rectificatives** i **[payouts](/payments/payouts-reconciliation)** per a la conciliació bancària. ## Diverses botigues [#multi-store] Un negoci pot operar diverses "botigues" o línies (una botiga física + un curs en línia) amb **comptes de Stripe diferents** (Stripe Connect) i voler **numeració de factures independent per a cadascuna** (`TIENDA-2026-…`, `CURSOS-2026-…`). Factuarea modela cada compte de Stripe que connectes com un **compte connectat**: cada Account Link que completes **afegeix** un compte — mai sobreescriu l'anterior — i els cobraments de cada compte s'enruten a **la sèrie i la configuració d'aquest compte**. Cada compte connectat porta: * una **sèrie** (`series_id`) usada per a les factures auto-creades a partir dels seus cobraments — `null` significa que s'usa la **sèrie de factures per defecte de l'empresa**; * la seva **pròpia configuració d'auto-facturació** (`autoinvoicing_enabled`, `simplified_threshold_cents`, `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`) — tota regla fiscal d'aquesta pàgina aplica per compte. Quan arriba un webhook, Factuarea resol el compte de Stripe (`acct_xxx`) al seu compte connectat i emet la factura **en la sèrie d'aquest compte**, amb la política fiscal d'aquest compte — de manera que dues botigues produeixen factures en dues sèries de numeració separades i correctes. ### Els comptes nous neixen segurs [#multi-store-defaults] Un compte acabat de connectar **no** hereta la configuració d'un altre compte: neix amb els mateixos valors per defecte segurs que una alta nova — auto-facturació **desactivada**, llindar **400 €**, "exigir NIF" desactivat, devolucions actives, subscripcions desactivades — i sense sèrie (cau en la sèrie per defecte de l'empresa fins que li n'assignes una). Configura'l explícitament abans que emeti res. ### Endpoints v1 per compte [#multi-store-api] Gestiona els comptes sota el recurs `connected-accounts` (mateixos scopes `stripe_autoinvoicing:read|write`; la identitat és l'`id` del compte, un UUID v7): * [Llistar comptes connectats](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.list) (`stripe_autoinvoicing:read`). * [Obtenir un compte connectat](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.show) (`stripe_autoinvoicing:read`). * [Actualitzar un compte connectat](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.update) — nom, `series_id` (envia `null` per netejar-la) i la configuració per compte (`stripe_autoinvoicing:write`). * [Desconnectar un compte connectat](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.disconnect) (`stripe_autoinvoicing:write`). ```bash # Assignar la sèrie CURSOS i activar l'auto-facturació en una botiga curl -X PUT https://api.factuarea.com/v1/connected-accounts/0192f3a4-… \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "series_id": "0192aaaa-…", "autoinvoicing_enabled": true }' ``` Desconnectar un compte conserva les seves factures ja emeses i el seu historial; els webhooks posteriors es registren **sense** processar. Referenciar l'`id` d'un compte que pertany a una altra empresa retorna `404` (`connected_account_not_found`) — l'aïllament multi-tenant mai filtra l'existència. ### L'endpoint a nivell d'empresa mentre tens una botiga [#multi-store-legacy] Els [endpoints de configuració](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show) a nivell d'empresa de dalt segueixen vàlids **mentre tens exactament un compte connectat**: el `GET` retorna la configuració efectiva d'aquest únic compte i el `PUT` fa de **proxy** cap a ell (escriu en la configuració del compte, mai una còpia duplicada a nivell d'empresa). Tan bon punt connectes un **segon** compte, la configuració a nivell d'empresa ja no pot respondre a "quin compte?". Tant el `GET` com el `PUT` retornen llavors `422` (`per_account_config_required`, missatge en castellà) apuntant-te als endpoints per compte — Factuarea **mai escriu en dos llocs**, així que no hi ha divergència entre una configuració a nivell d'empresa i els comptes. **Una sola font de veritat.** La configuració per compte és l'únic lloc on viuen els ajustaments. L'endpoint a nivell d'empresa és una comoditat que fa de proxy al compte únic; mai guarda una còpia separada, de manera que llegir i escriure sempre coincideixen. Amb dos o més comptes, usa `connected-accounts/{account}` directament. ## Factura ordinària o simplificada [#decision] Una factura espanyola necessita el **NIF** del destinatari per emetre's com a factura ordinària (F1). Un cobrament B2C sense NIF és justament el cas que la normativa resol amb una **factura simplificada (F2)**. Factuarea decideix quina emetre a partir de les dades que porta el cobrament més la teva política fiscal: | Situació | Factura emesa | | --------------------------------------------------------------------------------------------- | ----------------------------------------------- | | Es captura un **NIF vàlid** al Checkout, o el client resolt ja té un NIF a la seva fitxa | **Ordinària (F1)** | | **Sense NIF**, total del cobrament **igual o per sota** del llindar i "exigir NIF" desactivat | **Simplificada (F2)** | | **Sense NIF** i (total **per sobre** del llindar **o** "exigir NIF" activat) | **Revisió manual** — no s'auto-emet cap factura | Un NIF capturat es valida contra el format espanyol (NIF/NIE/CIF). Un NIF amb format no vàlid compta com a **sense NIF**, així que mai s'emet una F1 amb dades escombraries. Els cobraments derivats a revisió manual **no es perden**: queden registrats al log de la integració perquè emetis la factura a mà. L'auto-facturació continua amb la resta — un cobrament en revisió mai fa fallar el webhook. Tots apareixen llistats a la [safata d'esdeveniments d'integració](/payments/integration-events-inbox), que és on veus què li ha passat a cadascun. El sostre legal absolut d'una factura simplificada és de **3.000 €**, garantit pel propi domini de facturació: un cobrament sense NIF per sobre de 3.000 € sempre va a revisió manual, sigui quin sigui el llindar configurat. ## Capturar el NIF al Checkout [#nif-capture] Perquè un client que *sí* té NIF pugui aportar-lo, les Checkout Sessions que crea Factuarea (Flux A) activen la **recollida de l'identificador fiscal** de Stripe. El client pot introduir el seu `es_cif`/`eu_vat` en pagar, i aquest NIF encamina cap a la F1. El NIF també es llegeix de qualsevol `checkout.session.completed` entrant: * de `customer_details.tax_ids` (el camp estàndard de Stripe), i * dels **camps personalitzats** dels Payment Links que el comerç construeix al seu propi Dashboard de Stripe — Factuarea busca un camp la clau del qual sembli un identificador fiscal (`nif`, `dni`, `cif`, `vat`, `tax`). Un cobrament que arriba només com a `payment_intent.succeeded` (sense Checkout) no porta cap NIF capturat, però **encara** pot ser una F1 si el client es resol per email i ja té un NIF a la seva fitxa. ## El llindar [#threshold] `simplified_threshold_cents` és l'import **en cèntims** igual o per sota del qual un cobrament sense NIF s'auto-emet com a factura simplificada. El seu valor per defecte és **40000 (400 €)** i accepta qualsevol valor en el rang **\[0, 300000]** (0–3.000 €). Un valor per defecte conservador de 400 € és deliberat: el sostre de 3.000 € només és legal en sectors taxats concrets, i Factuarea no coneix el teu sector — apuja el llindar únicament si la teva activitat ho permet. ```bash curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "enabled": true, "simplified_threshold_cents": 100000, "require_nif": false }' ``` Tots dos camps fiscals són **opcionals**: si omets `simplified_threshold_cents` o `require_nif`, es conserven els seus valors actuals. Un llindar fora del rang retorna `422` (`validation_error`). ## Exigir un NIF [#require-nif] `require_nif` (per defecte `false`) té **dos efectes coherents**: * A les Checkout Sessions que crea Factuarea, l'identificador fiscal es marca com a **obligatori** (`if_supported`), de manera que se li sol·licita al client. * A la decisió de dalt, **veta la F2**: un cobrament sense NIF va a revisió manual en comptes de convertir-se en factura simplificada. Activa'l quan la teva empresa no vulgui mai factures simplificades automàtiques: cada cobrament passa llavors a tenir NIF (F1) o a esperar-te en revisió manual. Les factures auto-emeses són fiscalment reals i **irreversibles** (es crea el registre d'Alta de VeriFactu). Valida la teva política fiscal primer amb una clau `fact_test_`: al [sandbox](/guides/test-mode) el registre de VeriFactu es crea localment i mai es transmet a l'AEAT, així que pots exercitar la decisió F1/F2/revisió sense risc abans de passar a producció. ## L'esdeveniment sortint [#event] Cada factura auto-creada — F1 o F2 — emet l'esdeveniment [`invoice.auto_created`](/api-reference/events/public-api.v1.events.list) i un esdeveniment `payment.received`. En una factura simplificada el payload porta `client_id: null` (sense destinatari), de manera que un receptor de webhooks pot distingir la F1 de la F2. ## Desglossament real d'IVA amb Stripe Tax [#vat-breakdown] Si fas servir **Stripe Tax**, cada cobrament ja porta el desglossament fiscal real per línia — el tipus, la base imposable i, quan escau, la causa per la qual una línia està exempta o subjecta a inversió del subjecte passiu. Factuarea **reflecteix aquest desglossament** a la factura en lloc d'aplanar-ho tot a un únic tipus per defecte. El webhook del Checkout no inclou les línies, així que Factuarea fa una segona crida de només lectura a l'API en nom teu per recuperar-les amb els seus impostos, i mapeja cada línia: | Dada de Stripe Tax | Línia de la factura | | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `rate.percentage` | el tipus d'IVA de la línia (`vat_rate`), tal qual — mai no es recalcula | | `taxable_amount` | la base imposable de la línia (preu unitari = base ÷ quantitat) | | `taxability_reason` `zero_rated` / `product_exempt` / `customer_exempt` | línia **exempta** al 0 % | | `taxability_reason` `reverse_charge` | línia amb **inversió del subjecte passiu (ISP)** al 0 % | Així, un cobrament amb IVA mixt (p. ex. 21 % de consultoria + 10 % d'un llibre) es converteix en una factura amb **dues línies reals**, cadascuna al seu tipus, i el desglossament multi-IVA arriba fins al registre de VeriFactu. L'IVA **mai no es recalcula** — Stripe ja l'ha calculat, i recalcular-lo introduiria desviacions de cèntims. Factuarea agafa el tipus i la base imposable directament de Stripe Tax. **Quan no hi ha Stripe Tax** (no l'has activat al teu compte de Stripe) no canvia res: cada línia recorre a l'**IVA per defecte** de la teva empresa, igual que abans. Una empresa sense IVA per defecte configurat recorre al **0 %** — mai a un 21 % fantasma. ## Línies reals [#line-items] Quan un cobrament porta diverses línies (diversos productes o conceptes), apareixen com a **línies reals i independents** a la factura — cadascuna amb la seva descripció, quantitat i preu — en lloc de col·lapsar-se en una de sola. Són **línies lliures** (no enllaçades al teu catàleg de productes). Això és **ortogonal al tipus de factura**: la decisió F1/F2 de més amunt tria el *tipus*, el mapeig de línies tria les *línies* — tant una factura ordinària com una de simplificada obtenen les mateixes línies reals. Un cobrament que arriba només com a `payment_intent.succeeded` (sense Checkout Session, de manera que no hi ha línies recuperables), o un cobrament la recuperació de línies del qual falla després dels reintents, **continua produint una factura**: recorre a una sola línia amb l'IVA per defecte. La factura mai no es perd per un detall no essencial. Abans d'emetre, Factuarea **valida el total**: el total derivat de les línies reflectides (suma de subtotal + IVA per línia, en EUR) ha de coincidir amb l'import realment cobrat, dins d'una petita tolerància d'arrodoniment (±1 cèntim per línia, mínim ±0,05 €). Si no coincideix, el cobrament es deriva a **revisió manual** (`total_mismatch`) en lloc d'emetre una factura el total de la qual divergeixi del cobrament real — el webhook respon correctament igualment. ## Cobraments en una altra moneda [#currency] Un cobrament en una moneda diferent de l'EUR ja no es descarta. Factuarea el **converteix a EUR** fent servir el **tipus de canvi de referència del Banc Central Europeu (BCE)** de la data de pagament i emet la factura **en euros** — base, IVA i total, i el registre de VeriFactu/AEAT, tot en EUR (Art. 12.1 RD 1619/2012: la quota d'IVA s'ha de consignar en euros). * Un cobrament en **EUR** passa intacte. * Un cobrament **≠EUR** amb tipus disponible es converteix; la traça de la conversió — import i moneda originals, tipus BCE i data del tipus — s'escriu a les notes de la factura i al registre d'integració per a l'auditoria fiscal. * Un cobrament en una moneda **sense tipus BCE** disponible **no** s'auto-factura: es deriva a revisió manual i el webhook respon correctament igualment. Factuarea mai no inventa un tipus. L'entrada apareix a la [safata](/payments/integration-events-inbox#reasons) com a `unsupported_currency`, **aparcada**: quan es publiqui el tipus oficial d'aquella data, reprocessar l'esdeveniment emet la factura. Els tipus del BCE es memoritzen a diari (sense taula de base de dades addicional), de manera que diversos cobraments ≠EUR del mateix dia comparteixen una sola consulta del tipus. Emetre la factura **en la moneda original** (opció B) queda intencionadament fora d'abast — Factuarea sempre converteix a EUR (opció A). ## Devolucions i factures rectificatives [#refunds] Una factura emesa és fiscalment **irreversible** — mai s'esborra ni s'anul·la un cop pagada. L'única manera legal de desfer-la és una **factura rectificativa**. Per això, quan Stripe retorna un cobrament que Factuarea va facturar, Factuarea tanca el cicle fiscal per tu: el webhook `charge.refunded` genera **automàticament una factura rectificativa enllaçada** (amb el seu propi registre R de VeriFactu), sense rectificar a mà. Ho controla `refunds_enabled` (per defecte `true`). Només actua mentre l'auto-facturació està activada — el gating és `enabled && refunds_enabled`. Una empresa que mai va activar l'auto-facturació no veu cap canvi. | Devolució | Rectificativa emesa | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Total** (`refunded: true`) | Una rectificativa `total` que reflecteix tota la factura original com a línies negatives | | **Parcial** (`amount_refunded < amount`) | Una rectificativa `partial` amb una única línia negativa per l'import retornat, al tipus impositiu de la línia original | La rectificativa s'emet sempre **per diferències** (AEAT `TipoRectificativa: I`), perquè una devolució és un abonament amb imports negatius. Porta el motiu de correcció `devolucion`; si l'original és una factura simplificada (F2), la rectificativa s'emet com a **R5** automàticament. La factura original es localitza per dos camins — per la metadada `factuarea_invoice` del cobrament (Flux A) o per l'UUID determinista derivat del `payment_intent` (Flux B) — així que les devolucions de cobraments emesos **abans** que existís aquesta funció també es rectifiquen. **La idempotència és per devolució individual.** Stripe reenvia `charge.refunded` amb l'`amount_refunded` *acumulat*, però Factuarea es basa en l'id de cada devolució individual (`re_xxx`): cadascuna produeix **com a màxim una** rectificativa. Un esdeveniment reentregat, o una segona devolució parcial, mai abonen per duplicat. Una devolució la factura original de la qual no es pugui localitzar, que no estigui en un estat rectificable (`sent`/`paid`) o que ja estigui rectificada queda registrada a la [safata](/payments/integration-events-inbox) per a revisió manual — mai fa fallar el webhook. Cada rectificativa automàtica emet l'esdeveniment [`invoice.corrective_auto_created`](/api-reference/events/public-api.v1.events.list) (que porta la rectificativa, la factura original, el `refund_id` d'origen i el `provider`), i pots llistar-les amb [Llistar rectificatives auto-facturades](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list). Per rebre devolucions has d'habilitar l'esdeveniment **`charge.refunded`** a l'endpoint de webhook de Connect al Dashboard de Stripe. Com amb l'auto-facturació, valida el flux primer amb una clau `fact_test_`: al [sandbox](/guides/test-mode) el registre R de VeriFactu es crea localment i mai es transmet a l'AEAT. **Desactivar-ho aparca les devolucions, no les descarta.** Amb `refunds_enabled: false`, cada devolució queda registrada a la [safata](/payments/integration-events-inbox#reasons) com a `refund_autoinvoicing_disabled` i es conserva, xifrada, **30 dies**. Dins d'aquesta finestra tria **una** de les dues sortides, mai les dues: torna a posar `refunds_enabled` a `true` i [reprocessa](/payments/integration-events-inbox#replay) l'esdeveniment, **o** emet la rectificativa a mà. Fer les dues coses deixa la devolució amb **dues rectificatives**, cadascuna numerada a la seva sèrie i donada d'alta a VeriFactu — la idempotència del reprocés només reconeix les rectificatives emeses per aquesta mateixa via automàtica, així que una feta a mà no l'atura. Passats els 30 dies el contingut es purga i emetre-la a mà és l'única sortida que queda. ## Cicles de subscripció [#subscriptions] Si cobres amb **Stripe Billing** al teu compte connectat — subscripcions recurrents mensuals o anuals — Factuarea pot **emetre una factura automàticament per cada cicle cobrat**. Cada renovació que cobra Stripe produeix la seva pròpia factura conforme a VeriFactu, espejant el desglossament real (línies, període, impostos) que Stripe ja va calcular, igual que els cobraments únics. És un **toggle separat**, `subscription_autoinvoicing_enabled` (per defecte `false`), per sobre del flag general `enabled`. **Tots dos han d'estar actius** perquè un cicle es facturi — activar només les subscripcions no fa res mentre l'auto-facturació està globalment desactivada. ```bash curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "enabled": true, "subscription_autoinvoicing_enabled": true }' ``` ### Quins cicles es facturen [#subscription-cycles] Factuarea factura el **cicle de facturació estàndard** i deriva la resta a revisió o l'ignora, segons el `billing_reason` de Stripe: | `billing_reason` | Què fa Factuarea | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | `subscription_create` (primer cicle) | **Es factura** | | `subscription_cycle` (cada renovació) | **Es factura** | | `subscription_update`, `subscription_threshold` (prorrateigs standalone) | **Revisió manual** — es registra al log de la integració, no s'auto-factura | | `manual`, `upcoming`, `quote_accept`, altres | S'ignora amb un log informatiu | * **Els trials no facturen.** Un cicle el `invoice.total` del qual és **0 €** (un període de prova, o un cicle cobert íntegrament per crèdit) no emet factura; el primer cobrament real després del trial es factura amb normalitat. * **Els prorrateigs standalone** (un upgrade/downgrade cobrat pel seu compte, fora del cicle regular) **no** s'auto-facturen avui — van a revisió manual perquè decideixis. Apareixen a la [safata](/payments/integration-events-inbox#reasons) com a `subscription_proration_review`, que és accionable però **no** reprocessable: el `billing_reason` no canvia mai, així que la factura s'ha d'emetre a mà. El cicle regular següent es factura com sempre. * **La decisió F1/F2 és la mateixa.** El NIF del destinatari es llegeix dels `customer_tax_ids` de la invoice (més la fitxa del client resolt); amb NIF el cicle és una factura **ordinària (F1)**, sense ell i igual o per sota del llindar una **simplificada (F2)**, i sense ell per sobre del llindar (o amb "exigir NIF" activat) va a revisió manual — idèntic a les regles de la [secció de decisió](#decision). S'apliquen el mateix espejament de línies/IVA de Stripe Tax, la conversió a EUR i la validació de total. ### Cada cicle és la seva pròpia factura [#subscription-coexistence] Un cicle de subscripció es converteix en una **factura solta** — **no** crea ni toca cap **factura recurrent** de Factuarea. Les dues són independents: Stripe porta la cadència i cada `invoice.paid` produeix una factura. **Evita la doble facturació.** Si ja modeles la subscripció del mateix client com una **factura recurrent** manual a Factuarea, activar l'auto-facturació de subscripcions per a aquesta subscripció de Stripe produirà **dues factures per període** — una de la teva plantilla recurrent i una altra del cicle de Stripe. Tria una sola font per client: atura la factura recurrent manual o deixa aquest toggle desactivat per a aquestes subscripcions. ### L'esdeveniment sortint [#subscription-event] Una factura de cicle de subscripció emet un esdeveniment **diferent**, [`invoice.subscription_auto_created`](/api-reference/events/public-api.v1.events.list) (a més de `payment.received`), de manera que un receptor de webhooks pot distingir els cicles de subscripció dels cobraments únics. Un cobrament únic continua emetent `invoice.auto_created` com abans. El llistat de cobraments auto-facturats ([Llistar cobraments auto-facturats](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list)) exposa el context de subscripció (`subscription_id`, `stripe_invoice_id`, `period_start`, `period_end`) per als cobraments de cicle (`null` per a cobraments únics), i un filtre opcional `origin` (`subscription`/`oneshot`). Aquest mateix context viatja també **a la factura mateixa**, com a claus de metadata de sistema per les quals pots filtrar els llistats. A [Metadata i conciliació](/payments/metadata-reconciliation) hi tens la taula completa —quines claus s'escriuen sempre i quines s'ometen quan no apliquen— i dues receptes a punt: totes les factures d'una subscripció, i les factures d'un sol període de facturació. **La idempotència és per cicle de facturació.** Factuarea es basa en cada id d'invoice de Stripe (`in_xxx`): un esdeveniment reenviat, o un segon esdeveniment del mateix cicle, produeix **com a màxim una** factura. **Un cicle que va arribar amb el toggle apagat encara es pot facturar — durant 30 dies.** No es descarta en silenci: queda registrat a la [safata](/payments/integration-events-inbox#reasons) com a `subscription_autoinvoicing_disabled`, amb el contingut desat xifrat. Activa `subscription_autoinvoicing_enabled` (i `enabled`) i [reprocessa](/payments/integration-events-inbox#replay) l'esdeveniment dins d'aquesta finestra, i s'emet la **factura real del cicle** — amb el seu número de sèrie i el seu registre VeriFactu, igual que si s'hagués facturat en el seu moment. Passats els 30 dies el contingut es purga, la fila es manté i l'única sortida és emetre la factura a mà. Els cicles cobrats abans que el teu endpoint de Connect comencés a enviar `invoice.paid` no van arribar mai a Factuarea, així que allà no hi ha res a reprocessar. Per rebre els cicles de subscripció has d'habilitar l'esdeveniment **`invoice.paid`** a l'endpoint de webhook de Connect al Dashboard de Stripe. Com sempre, valida el flux primer amb una clau `fact_test_`: al [sandbox](/guides/test-mode) el registre d'Alta de VeriFactu es crea localment i mai no es transmet a l'AEAT. ## Valors per defecte d'un cop d'ull [#defaults] | Camp | Per defecte | Efecte del valor per defecte | | ------------------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------ | | `enabled` | `false` | Auto-facturació desactivada — no s'emet res fins que l'activis | | `simplified_threshold_cents` | `40000` (400 €) | Els cobraments sense NIF de fins a 400 € es converteixen en F2 | | `require_nif` | `false` | Es permeten factures simplificades; l'identificador fiscal s'ofereix però no s'exigeix | | `refunds_enabled` | `true` | Una devolució de Stripe genera una factura rectificativa automàtica (mentre l'auto-facturació està activada) | | `subscription_autoinvoicing_enabled` | `false` | Els cicles de subscripció de Stripe no s'auto-facturen fins que ho activis (requereix també `enabled`) | Amb aquests valors per defecte, l'únic canvi de comportament un cop activada l'auto-facturació és que un cobrament sense NIF de fins a 400 € es converteix en factura simplificada en comptes d'esperar en revisió, i una devolució genera la seva factura rectificativa automàticament. Els cobraments amb NIF es comporten exactament igual que abans. Els cicles de subscripció queden desactivats fins que optis per ells amb `subscription_autoinvoicing_enabled`. --- # Preus i límits de l'API (/ca/pricing) L'API pública i el servidor MCP estan **inclosos en tots els plans de pagament de Factuarea**. No hi ha cap complement per contractar ni sol·licitud d'accés: crea una clau des de [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) i comença a cridar `/v1`. El que sí que pots pagar és **capacitat**. El teu pla atorga un tier de límits; si en necessites un de superior sense canviar de pla, subscriu-te a un [boost de capacitat](#capacity-boost). La columna de preu de sota és el preu d'aquest boost, mai el preu de l'accés. ## Nivells [#nivells] | Tier | Preu del boost | Per minut | Per mes | API keys actives | Endpoints de webhook | | ----------- | -------------------------- | ------------- | ------------- | ---------------- | -------------------- | | **Free** | No es ven (tier del trial) | 10 rpm | 100 | 1 | 0 | | **Starter** | 4,90 € / mes | 30 rpm | 5.000 | 3 | 1 | | **Pro** | 19,90 € / mes | 300 rpm | 50.000 | 25 | 10 | | **Scale** | Sales-led | Personalitzat | Personalitzat | Il·limitades | Il·limitats | Xifres verificades contra la configuració de tiers del backend el **2026-07-31**. Per saber com es comporten les quotes —finestra lliscant, capçaleres `X-RateLimit-*`, el codi `429` i l'estratègia de reintent— consulta [Límits de peticions](/guides/rate-limits). ## Què atorga ja el teu pla [#què-atorga-ja-el-teu-pla] | El teu pla | Tier atorgat sense cost addicional | | --------------------------------------- | ---------------------------------- | | Trial (encara sense subscripció activa) | Free | | Emprendedor | Starter | | Empresario | Pro | | Enterprise | Scale | El tier segueix el pla tot sol: no es tria per clau ni per petició, i canvia tan bon punt canvia el teu pla. ## Boost de capacitat [#capacity-boost] Un boost compra un tier **estrictament superior** al que ja atorga el teu pla —per exemple Starter → Pro en el pla Emprendedor—. Es contracta des de [Dashboard → Developers → Upgrade](https://app.factuarea.com/settings/developers/upgrade) i es factura mensualment. Mentre el boost està actiu, totes les claus de l'empresa fan servir el tier del boost. Com que el pla ja atorga un tier, comprar-ne un d'igual o inferior al seu es rebutja: la regla i l'error que retorna són a [Límits de peticions → Boost de capacitat](/guides/rate-limits#capacity-boost). ## Topalls que no són peticions [#topalls-que-no-són-peticions] Hi ha dos límits que es compten per empresa, no per petició. ### API keys actives [#api-keys-actives] Una clau compta mentre no estigui revocada ni caducada; revocar-ne una allibera lloc a l'instant. Crear-ne una per sobre del topall respon `422` amb `code: max_api_keys_exceeded`, indicant el tier i el topall. Per superar-ho, revoca una clau que ja no facis servir o puja de tier. ### Endpoints de webhook [#endpoints-de-webhook] Un endpoint compta mentre és viu (`active` o `degraded`); un de deshabilitat o esborrat no compta. Crear-ne un per sobre del topall respon `422` amb `code: business_rule_violation` i `subcode: max_webhook_endpoints_reached`. Al tier **Free** el topall és `0`, així que ja falla el primer endpoint —i ho fa amb `402 addon_required` en comptes del `422` de dalt, perquè no hi ha res per alliberar—. Els webhooks requereixen un pla de pagament (Starter o superior) o un boost de capacitat. ## El mode test no és un tier més barat [#el-mode-test-no-és-un-tier-més-barat] Una clau `fact_test_` porta el **mateix tier** que les teves claus de producció, així que al trànsit de sandbox se li apliquen les mateixes quotes per minut i mensual. En mode test no s'eximeix cap límit de peticions. El que el sandbox treu és l'efecte real, no la quota: els registres de VeriFactu es creen en local i **mai no es transmeten a l'AEAT**, els correus de documents no arriben a destinataris reals i els esdeveniments queden registrats però no s'entreguen als teus endpoints. Tens la llista completa a [Mode test i sandbox](/guides/test-mode). Un topall sí que es comporta diferent: les claus de test viuen en una empresa sandbox a part, així que consumeixen la quota de claus d'aquella empresa i no la de la teva empresa real. ## Volum alt [#volum-alt] **Scale** no té preu publicat: els topalls s'acorden cas per cas. Escriu a [info@factuarea.com](mailto:info@factuarea.com) des del correu associat a la teva empresa a Factuarea, amb el volum de peticions que esperes i els endpoints que faràs servir. El que porta el tier: quotes per minut i mensual personalitzades, claus actives i endpoints de webhook il·limitats, acord de nivell de servei i gestor de compte dedicat. ## Com veure el que consumeixes [#com-veure-el-que-consumeixes] [Dashboard → Developers → Usage](https://app.factuarea.com/settings/developers/usage) mostra el teu consum enfront del tier actual. Cada resposta de l'API porta a més les capçaleres `X-RateLimit-*`, la manera més barata de detectar que t'acostes al límit abans d'arribar-hi: com llegir-les és a [Límits de peticions](/guides/rate-limits). --- # Resum dels SDKs (/ca/sdks) Factuarea ofereix **SDKs oficials** que envolten tota l'API REST v1 ( operacions repartides en recursos) amb un runtime premium perquè no hagis d'escriure HTTP a mà: reintents automàtics, idempotency keys automàtiques, auto‑paginació per cursor transparent, una jerarquia d'errors tipada, verificació de webhooks tipada i descàrregues binàries (PDF). `@factuarea/sdk` a npm. ESM + CommonJS dual, declaracions de tipus completes. Codi font: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). `factuarea/factuarea-php` a Packagist. PSR‑4, basat en Guzzle, PHP 8.2+. Codi font: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). **Pre‑GA (`0.x`).** Tots dos SDKs estan a `0.x`. La superfície pública de mètodes és estable i segueix el [contracte de nomenclatura de mètodes del SDK](https://github.com/factuarea), protegit per SemVer — però mentre estigui a `0.x`, les versions minor poden incloure canvis incompatibles fins a `1.0.0`, que coincideix amb la GA de l'API. Cada release fixa una [`Factuarea-Version`](/guides/versioning) i l'envia a cada request, de manera que el comportament de l'API es manté estable fins que actualitzes el SDK. **Només al servidor.** La teva API key és un secret. No incloguis mai un SDK amb una clau live en un navegador, app mòbil o qualsevol client públic — fes servir el SDK des del teu backend. ## Instal·lació [#installació] ```bash npm install @factuarea/sdk ``` Requereix **Node 20 o superior**. El SDK està construït sobre l'estàndard Web `fetch`, així que també funciona a Deno, Bun i Cloudflare Workers. ```bash composer require factuarea/factuarea-php ``` Requereix **PHP 8.2 o superior** amb les extensions `json` i `mbstring` (totes dues incloses a les builds estàndard de PHP). ## Autenticació i entorns [#autenticació-i-entorns] Passa la teva API key. **El prefix de la clau selecciona l'entorn** — no hi ha cap flag a part: una clau `fact_test_…` sempre s'executa contra el [sandbox](/guides/test-mode) aïllat, i una clau `fact_live_…` contra producció. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); factuarea.environment; // "test" or "live", derived from the key prefix ``` Configuració opcional: ```ts new Factuarea({ apiKey: "fact_live_…", // required baseUrl: "https://api.factuarea.com/v1", // override for staging timeout: 60_000, // per-request ms (default 60s) maxRetries: 2, // attempts after the first try factuareaVersion: "2026-06-04", // pinned API version header defaultHeaders: {}, // extra headers on every request }); ``` ```php setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY'))) ->setServerURL('https://api.factuarea.com/v1') ->build(); ``` ## Inici ràpid [#inici-ràpid] Crea un client i una factura, i després descarrega'n el PDF. Cada operació és accessible com a `.` (TypeScript) o `->{resource}->publicApiV1{Resource}{Action}` (PHP) seguint el contracte de nomenclatura — els snippets per endpoint de la referència de l'API mostren la crida exacta per a cada operació. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Responses are the API's `{ data: … }` envelope — read the resource off `.data`. // 1. Create a client. const { data: client } = await factuarea.clients.create({ name: "Cliente Demo SL", tax_id: "B98765432", }); // 2. Create an invoice (the API computes the totals). const { data: invoice } = await factuarea.invoices.create({ client_id: client.id, series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", issued_on: "2026-06-05", due_on: "2026-07-05", lines: [ { description: "Consultoría — junio 2026", quantity: 10, unit_price: 100, tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", }, ], }); // 3. Download the PDF (a BinaryResponse, not JSON). const pdf = await factuarea.invoices.pdf(invoice.id); await import("node:fs/promises").then((fs) => fs.writeFile("invoice.pdf", pdf.toBuffer()), ); ``` ```php clients->publicApiV1ClientsCreate( new Components\CreateClientRequest( name: 'Cliente Demo SL', taxId: 'B98765432', ), ); // 2. Create an invoice (the API computes the totals). $invoice = $factuarea->invoices->publicApiV1InvoicesCreate( new Components\CreateInvoiceRequest( clientId: $client->object->data->id, seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e', issuedOn: LocalDate::parse('2026-06-05'), dueOn: LocalDate::parse('2026-07-05'), lines: [ new Components\CreateInvoiceRequestLine( description: 'Consultoría — junio 2026', quantity: 10, unitPrice: 100, taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f', ), ], ), ); // 3. Download the PDF. $pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id); file_put_contents('invoice.pdf', $pdf->bytes ?? ''); ``` Executa-ho tot primer amb una clau **`fact_test_`** — els efectes del sandbox (VeriFactu → AEAT, FACe, email, webhooks) estan desactivats. Quan el teu flux funcioni d'extrem a extrem, canvia el prefix a `fact_live_`. La superfície de l'API és idèntica en tots dos. Consulta [Test mode & sandbox](/guides/test-mode). ## Funcions en temps d'execució [#funcions-en-temps-dexecució] Tots dos SDKs comparteixen el mateix runtime escrit a mà sobre la superfície tipada generada: * **Reintents automàtics** — les fallades transitòries (`429` i `5xx`, a més d'errors de xarxa a TypeScript) es reintenten amb backoff exponencial i jitter, respectant el header `Retry-After`. Els errors de client deterministes (p. ex. validació `422`) **mai** es reintenten. * **Idempotència automàtica** — cada mutació rep una `Idempotency-Key` generada perquè un request reintentat mai creï un recurs per duplicat. Sobreescriu-la per crida quan vulguis deduplicació a nivell d'app. Consulta [Idempotency](/guides/idempotency). * **Auto‑paginació per cursor** — els mètodes de llistat retornen un iterable que recorre totes les pàgines per tu, gestionant `next_cursor` / `has_more`. Consulta [Paginar amb el SDK](#paginating-with-the-sdk). * **Errors tipats** — l'[embolcall d'error](/guides/errors) de l'API es mapeja a una jerarquia d'excepcions tipada que exposa `code`, `type`, `request_id` i `status`. La teva API key mai s'inclou en cap missatge d'error. Consulta [Gestionar errors](#handling-errors). * **Verificació de webhooks** — un verificador HMAC‑SHA256 de temps constant que respecta la finestra de gràcia de rotació de secret. Consulta [Verificar webhooks](#verifying-webhooks). * **Descàrregues binàries** — els endpoints de PDF i de fitxers retornen una resposta binària que converteixes en un Buffer / stream, no JSON. ## Paginació amb el SDK [#paginating-with-the-sdk] Els mètodes de llistat retornen un `Page`, que és en si mateix un async iterable: ```ts const page = await factuarea.invoices.list({ status: "paid", limit: 50 }); // (a) iterate every item across every page for await (const invoice of page) { console.log(invoice.id); } // (b) page by page page.data; // items on this page page.hasMore; // boolean page.nextCursor; // opaque cursor or null const next = await page.getNextPage(); // Page | null // (c) collect everything into an array const all = await page.toArray(); ``` El helper `PageIterator` transmet cada element a través de totes les pàgines sense gestió manual del cursor: ```php use Factuarea\Sdk\Custom\Pagination\PageIterator; use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest; $pages = new PageIterator( fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList( new PublicApiV1InvoicesListRequest(startingAfter: $cursor), )->rawResponse, ); // items() yields each item as a decoded associative array. foreach ($pages->items() as $invoice) { echo $invoice['id'], PHP_EOL; } ``` Consulta [Pagination](/guides/pagination) per conèixer la semàntica de cursor subjacent. ## Gestió d'errors [#handling-errors] ```ts import { FactuareaError, ValidationError, RateLimitError, } from "@factuarea/sdk"; try { await factuarea.invoices.create(body); } catch (error) { if (error instanceof ValidationError) { console.error(error.fields); // { tax_id: ["NIF inválido"], … } } else if (error instanceof RateLimitError) { console.error(error.retryAfter); // seconds to wait } else if (error instanceof FactuareaError) { console.error(error.code, error.requestId); } } ``` ```php use Factuarea\Sdk\Models\Errors\ErrorThrowable; try { $factuarea->invoices->publicApiV1InvoicesCreate($body); } catch (ErrorThrowable $e) { $error = $e->container->error; echo $error->type->value; // e.g. "invalid_request_error" echo $error->code; // e.g. "parameter_invalid" echo $error->param; // e.g. "client_id" echo $error->requestId; // quote this to support } ``` Ramifica segons el `code` estable, mai segons el `message` en castellà orientat a persones. El catàleg complet és a [Errors](/guides/errors). ## Verificació de webhooks [#verifying-webhooks] Passa el **cos cru del request** (no un objecte re‑serialitzat), el header `Factuarea-Signature` i el secret de l'endpoint: ```ts import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Express, with express.raw({ type: "application/json" }) on the route: app.post("/webhooks/factuarea", (req, res) => { try { const event = factuarea.webhooks.verify( req.body.toString("utf8"), req.headers[SIGNATURE_HEADER.toLowerCase()] as string, process.env.FACTUAREA_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { /* … */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` ```php use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier; use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException; $verifier = new WebhookVerifier(); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; try { $event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET')); // $event is the decoded, authenticated payload } catch (WebhookSignatureException $e) { http_response_code(400); } ``` La verificació fa servir HMAC‑SHA256 amb una comparació de temps constant i una tolerància de timestamp configurable (5 minuts per defecte) per rebutjar replays, i accepta totes dues firmes durant una finestra de gràcia de rotació de secret. Consulta [Webhooks](/guides/webhooks). ## Snippets per endpoint [#snippets-per-endpoint] Cada pàgina de la referència de l'API mostra un snippet de **TypeScript**, **PHP** i **cURL** llest per copiar per a aquesta operació exacta, generat a partir del spec perquè mai es desviïn de la superfície real. ## Genera el teu propi client [#generate-your-own-client] Si el teu llenguatge encara no està cobert, o prefereixes un client que tu controlis i desis al teu repo, el contracte canònic llegible per màquina és el spec **OpenAPI 3.1** — apunta-hi qualsevol generador. El spec viu a [`https://docs.factuarea.com/api/openapi`](/api/openapi). Es genera a partir del mateix backend que serveix l'API, així que mai es desvia de la superfície real. ```bash npx openapi-typescript https://docs.factuarea.com/api/openapi \ -o src/factuarea.d.ts ``` ```bash openapi-python-client generate \ --url https://docs.factuarea.com/api/openapi ``` ```bash openapi-generator-cli generate \ -i https://docs.factuarea.com/api/openapi \ -g -o ./factuarea-client ``` `` pot ser qualsevol [generador suportat](https://openapi-generator.tech/docs/generators) — Go, Java, C#, Ruby, Rust i més. Un client generat no inclourà el runtime del SDK oficial (reintents, idempotència, paginació, verificació de webhooks) — això ho connectes tu mateix seguint les [guies de conceptes clau](/guides/idempotency). ## Construeixes amb un assistent d'IA? [#construeixes-amb-un-assistent-dia] Si vols que un agent d'IA operi Factuarea directament en lloc de generar codi de client, connecta'l al [servidor MCP](/mcp) — l'API pública exposada com a tools, amb autenticació OAuth i per API key. Per a Claude Code, el [plugin](/mcp/claude-code-plugin) oficial `factuarea-mcp` ho configura en dues comandes. --- # PHP (/ca/sdks/php) L'SDK oficial de PHP és [`factuarea/factuarea-php`](https://packagist.org/packages/factuarea/factuarea-php) a Packagist — PSR-4, basat en Guzzle. Codi font: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). Embolcalla l'API REST v1 amb reintents automàtics, idempotency keys, auto-paginació per cursor, una jerarquia d'errors tipada i verificació de webhooks — tot plegat descrit a la [introducció a l'SDK](/sdks). ## Instal·lació [#installació] ```bash composer require factuarea/factuarea-php ``` Requereix **PHP 8.2 o superior** amb les extensions `json` i `mbstring` (totes dues incloses a les builds estàndard de PHP). ## Autenticació [#autenticació] Passa la teva API key. **El prefix de la clau selecciona l'entorn** — no hi ha cap flag a part: una clau `fact_test_…` sempre s'executa contra el [sandbox](/guides/test-mode) aïllat, i una clau `fact_live_…` contra producció. ```php setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY'))) ->setServerURL('https://api.factuarea.com/v1') ->build(); ``` **Només al servidor.** La teva API key és un secret. No distribueixis mai l'SDK amb una clau live en un client públic — fes-la servir des del teu backend. ## Inici ràpid [#inici-ràpid] Crea un client i una factura, i després descarrega'n el PDF. Cada operació és accessible com a `->{resource}->publicApiV1{Resource}{Action}`; els snippets per endpoint de la referència de l'API mostren la crida exacta per a cada operació. ```php clients->publicApiV1ClientsCreate( new Components\CreateClientRequest( name: 'Cliente Demo SL', taxId: 'B98765432', ), ); // 2. Create an invoice (the API computes the totals). $invoice = $factuarea->invoices->publicApiV1InvoicesCreate( new Components\CreateInvoiceRequest( clientId: $client->object->data->id, seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e', issuedOn: LocalDate::parse('2026-06-05'), dueOn: LocalDate::parse('2026-07-05'), lines: [ new Components\CreateInvoiceRequestLine( description: 'Consultoría — junio 2026', quantity: 10, unitPrice: 100, taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f', ), ], ), ); // 3. Download the PDF. $pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id); file_put_contents('invoice.pdf', $pdf->bytes ?? ''); ``` Executa-ho tot primer amb una clau **`fact_test_`** — els efectes del sandbox (VeriFactu → AEAT, FACe, email, webhooks) estan desactivats. Quan el teu flux funcioni d'extrem a extrem, canvia el prefix a `fact_live_`. Consulta [Mode de prova i sandbox](/guides/test-mode). ## Següents passos [#següents-passos] El comportament en runtime — reintents, idempotència, auto-paginació per cursor, la jerarquia d'errors tipada i la verificació de webhooks — és comú a tots dos SDK i està documentat una sola vegada a la [introducció a l'SDK](/sdks): * [Característiques de runtime](/sdks#runtime-features) * [Paginar amb l'SDK](/sdks#paginating-with-the-sdk) * [Gestió d'errors](/sdks#handling-errors) * [Verificar webhooks](/sdks#verifying-webhooks) --- # TypeScript (/ca/sdks/typescript) L'SDK oficial de TypeScript és [`@factuarea/sdk`](https://www.npmjs.com/package/@factuarea/sdk) a npm — ESM + CommonJS dual amb declaracions de tipus completes. Codi font: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). Embolcalla l'API REST v1 amb reintents automàtics, idempotency keys, auto-paginació per cursor, una jerarquia d'errors tipada i verificació de webhooks — tot cobert a la [introducció a l'SDK](/sdks). ## Instal·lació [#installació] ```bash npm install @factuarea/sdk ``` Requereix **Node 20 o superior**. L'SDK està construït sobre l'estàndard Web `fetch`, així que també funciona a Deno, Bun i Cloudflare Workers. ## Autenticació [#autenticació] Passa la teva API key. **El prefix de la clau selecciona l'entorn** — no hi ha un flag a part: una clau `fact_test_…` sempre s'executa contra el [sandbox](/guides/test-mode) aïllat, i una clau `fact_live_…` contra producció. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); factuarea.environment; // "test" or "live", derived from the key prefix ``` Configuració opcional: ```ts new Factuarea({ apiKey: "fact_live_…", // required baseUrl: "https://api.factuarea.com/v1", // override for staging timeout: 60_000, // per-request ms (default 60s) maxRetries: 2, // attempts after the first try factuareaVersion: "2026-06-04", // pinned API version header defaultHeaders: {}, // extra headers on every request }); ``` **Només al servidor.** La teva API key és un secret. No distribueixis mai l'SDK amb una clau live a un navegador, app mòbil o qualsevol client públic — fes-la servir des del teu backend. ## Inici ràpid [#inici-ràpid] Crea un client i una factura, després descarrega'n el PDF. Cada operació és accessible com a `.`; els snippets per endpoint de la referència de l'API mostren la crida exacta per a cada operació. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Responses are the API's `{ data: … }` envelope — read the resource off `.data`. // 1. Create a client. const { data: client } = await factuarea.clients.create({ name: "Cliente Demo SL", tax_id: "B98765432", }); // 2. Create an invoice (the API computes the totals). const { data: invoice } = await factuarea.invoices.create({ client_id: client.id, series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", issued_on: "2026-06-05", due_on: "2026-07-05", lines: [ { description: "Consultoría — junio 2026", quantity: 10, unit_price: 100, tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", }, ], }); // 3. Download the PDF (a BinaryResponse, not JSON). const pdf = await factuarea.invoices.pdf(invoice.id); await import("node:fs/promises").then((fs) => fs.writeFile("invoice.pdf", pdf.toBuffer()), ); ``` Executa-ho tot primer amb una clau **`fact_test_`** — els efectes del sandbox (VeriFactu → AEAT, FACe, email, webhooks) estan desactivats. Quan el teu flux funcioni d'extrem a extrem, canvia el prefix a `fact_live_`. Consulta [Mode de prova i sandbox](/guides/test-mode). ## Pròxims passos [#pròxims-passos] El comportament en temps d'execució — reintents, idempotència, auto-paginació per cursor, la jerarquia d'errors tipada i la verificació de webhooks — és comú a tots dos SDK i està documentat una sola vegada a la [introducció a l'SDK](/sdks): * [Funcionalitats en temps d'execució](/sdks#runtime-features) * [Paginar amb l'SDK](/sdks#paginating-with-the-sdk) * [Gestió d'errors](/sdks#handling-errors) * [Verificar webhooks](/sdks#verifying-webhooks) --- # Suport (/ca/support) Aquesta pàgina és l'única font de veritat sobre com contactar amb l'equip de l'API de Factuarea i què enviar perquè puguem ajudar-te ràpid. ## Contacte [#contacte] El canal per a tot el que estigui relacionat amb l'API: dubtes d'integració, reports de bugs i incidències. Escriu des de l'email associat a la teva empresa a Factuarea. ## Què incloure en reportar un problema [#què-incloure-en-reportar-un-problema] Cada resposta de l'API porta un `request_id` únic (a l'embolcall d'error sota `error.request_id`, i a la capçalera de resposta `X-Request-Id`). És el més útil que ens pots enviar — ens permet correlacionar logs, mètriques i traces per investigar ràpidament. ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "El campo client_id es obligatorio.", "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA" } } ``` Un bon report inclou: * **`request_id`** de la crida que falla (o diversos, si és intermitent). * **HTTP status** i el `type` / `code` de l'embolcall d'error. * **Endpoint i mètode** — p. ex. `POST /v1/invoices`. * **Entorn** — `live` o `test` (el prefix de la key que vas fer servir, `fact_live_` o `fact_test_`). No enganxis mai el secret de la key. * **Què esperaves** vs. què va passar, i la marca de temps aproximada. No comparteixis mai el secret d'una API key en un email de suport. Envia el `request_id` — podem trobar la key i la petició només amb això. Si un secret ha quedat exposat, [rota o revoca la key](/guides/authentication) des del dashboard primer. Un assumpte que ja porti l'essencial ens ajuda a triar: ``` 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV ``` ## Accés a l'API [#accés-a-lapi] L'API pública i el servidor MCP estan **inclosos en tots els plans de Factuarea** — no hi ha programa beta ni add-on a banda. Crea les teves keys des de [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys); el teu tier de rate limit es deriva del teu pla (consulta [Límits de peticions](/guides/rate-limits)). Si les teves crides retornen `403 addon_not_active`, la teva empresa no té un pla actiu que inclogui accés a l'API — contracta o renova un pla des del dashboard. ## Pàgina d'estat [#pàgina-destat] Una pàgina d'estat pública (uptime i historial d'incidències) viurà a **status.factuarea.com**. Fins llavors, avisem les empreses afectades de les incidències i del manteniment planificat directament per email als contactes registrats de les keys. ## Changelog [#changelog] Cada canvi a `/v1` — camps nous, endpoints nous, esdeveniments nous, correccions de validació i deprecacions — es publica al [Changelog](/changelog/launch). Els breaking changes mai no arriben a `/v1`; només apareixen en una futura `/v2`. Consulta [Versionat](/guides/versioning) per conèixer el compromís d'estabilitat. ## Self-service primer [#self-service-primer] Abans d'obrir un tiquet, això sol respondre la pregunta més de pressa: * [FAQ](/faq) — les preguntes d'integració més habituals. * [Errors](/guides/errors) — busca el teu `code` per veure la causa i la solució. * [Autenticació](/guides/authentication) — keys, scopes, rotació. * [Límits de peticions](/guides/rate-limits) — quotes i back-off. --- # Launch (/changelog/launch) ## Time tracking (control horario) — 2026-07-11 [#time-tracking-control-horario--2026-07-11] Factuarea now covers the Spanish employer's duty to keep a daily working-time record — **RD-ley 8/2019**, art. 34.9 of the Workers' Statute — and exposes the whole workforce system over the same v1 contract. It is the **VeriFactu of attendance**: an append-only ledger sealed by a per-company SHA-256 hash chain, where nothing is ever edited or deleted and any tampering breaks the chain. The whole surface is gated by the new **`control_horario` module**. Start with the [Time tracking overview](/guides/workforce-overview). * **Eight new domains** — employees (with invitations and per-seat billing), work schedules, time entries (clock in/out, pauses, retroactive entries and corrections), monthly register closes, payroll exports, absences (types, policies, requests, balances and calendar), presence, and public holidays. * **New scopes** — a dedicated set in the closed catalog: `employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read` and `payroll_exports:read`, all behind the `control_horario` module. See [Scopes & irreversibility](/guides/scopes-and-irreversibility). * **Sealed monthly close** — freeze a finished month, then seal it with a detached RSA-SHA256 signature over the snapshot; the seal is irreversible (one per close) and independently verifiable. Export the daily record in the `rdley_8_2019` format, or a payroll incidents file for A3, Sage or NominaSOL. See [Monthly time-record close](/guides/monthly-time-close). * **Portal-only employee role** — an employee clocks in, follows a schedule and requests absences from the portal, and **never** counts against the plan `users` limit. * **Per-seat add-on** — employees are billed through a dedicated monthly `employee-seats` subscription whose quantity follows your active roster; contracting it activates the module. An enterprise account billed by contract gets it for free. See [Employee seat billing](/guides/employee-seats). * **MCP parity** — every v1 route mirrors a public MCP tool, so an agent drives the same operations. See the [MCP tools catalog](/mcp/tools#employee). Two domains are **read-only** over the API — presence and public holidays expose only reads. Declaring office/remote presence and creating custom local holidays are portal-only tasks, with no `presence:write` nor `holidays:write` scope. ## API and MCP included in every plan — 2026-07-04 [#api-and-mcp-included-in-every-plan--2026-07-04] The public API and the MCP server are no longer sold as a separate `developer_api` add-on — they are now **included in every Factuarea plan**: * **Tier per plan** — your rate-limit tier is derived from your plan: Emprendedor → `starter` (30 req/min, 5,000 req/month), Empresario → `pro` (300 req/min, 50,000 req/month), Enterprise → `scale` (custom, no caps). See [Rate limits](/guides/rate-limits). * **Trial included** — during the 10-day trial you get API access on the `free` tier (10 req/min, 100 req/month). * **Capacity boost** — if you need more capacity without changing plans, subscribe from the dashboard to a tier strictly higher than the one your plan grants; a tier equal to or lower returns `422 boost_not_applicable`. See [Capacity boost](/guides/rate-limits#capacity-boost). * **The add-on is gone** — the Starter and Pro developer add-ons are no longer sold. The `addon_not_active` error code stays (it now means the company has no active plan that includes API access), so existing integrations need no change. * **Beta program closed** — API access no longer requires a request: create a key from [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) and start calling `/v1`. **v1** — released 2026-05-03. This is the first public release of the Factuarea platform; everything below ships together. Future releases are added to this page, newest first, each headed by its version and date. For the first time you can integrate Factuarea with any external system — by code, by SDK, by command line or by AI agent — without scraping or macros. The public surface is one contract at `https://api.factuarea.com/v1`, reachable four ways: the REST API, the TypeScript and PHP SDKs, the `factuarea` CLI, and the MCP server. Each surface talks to the same resources and enforces the same scopes. ## REST API v1 [#rest-api-v1] The public REST API exposes ** operations across resources** as plain JSON over HTTPS. Every resource is identified by an opaque `id` (a UUID v7 string). ### Sales documents [#sales-documents] * **Invoices** (`/v1/invoices`) — full CRUD and the complete lifecycle: send, mark paid, void, annul, duplicate, PDF and public link, payments and receipts, reminders. Corrective invoices (rectificativas) with the `R1`–`R5` correction reason codes, simplified-invoice eligibility and substitution, scheduled issuing (schedule / reschedule / unschedule), and quarterly export (ZIP and email). Bulk create, send, status change, delete and PDF, plus Excel export. * **Quotes** (`/v1/quotes`) — CRUD + accept, reject, convert to invoice, PDF, public link. * **Pro-forma invoices** (`/v1/proformas`) — CRUD + convert to invoice, PDF, public link. * **Delivery notes** (`/v1/delivery_notes`) — CRUD + sign, mark delivered, convert to invoice. * **Recurring invoices** (`/v1/recurring_invoices`) — CRUD + activate, pause, resume, cancel and preview the next occurrence. ### Purchases [#purchases] * **Purchase invoices** (`/v1/purchase_invoices`) — CRUD with PDF attachment, mark paid, payment registration, and pending / overdue reporting. ### CRM and catalog [#crm-and-catalog] * **Clients** (`/v1/clients`) — full CRUD, search by tax ID (NIF/CIF), AEAT census and VIES verification, and CSV import with a downloadable template. * **Suppliers** (`/v1/suppliers`) — full CRUD, search by tax ID. * **Products** (`/v1/products`) — CRUD, lookup by SKU or external id, stock control (set, adjust and bulk update), low-stock report, sales analytics, and gallery images and video. * **Document series** (`/v1/series`) — legal numbering series per document type, with monthly / annual reset, default selection and archive / unarchive. * **Taxes** (`/v1/taxes`) — tax rates (IVA, IRPF retention, equivalence surcharge) with per-document defaults. ### Spanish tax compliance [#spanish-tax-compliance] * **VeriFactu** (`/v1/verifactu/*`, `/v1/invoices/{invoice}/verifactu`) — billing records, the SIF hash chain and its validation, subsanación (correction records), the declaración responsable and its history, and FNMT certificate management. * **FacturaE / FACe** (`/v1/invoices/{invoice}/facturae`, `/v1/face-submissions`) — FacturaE 3.2.2 XML download and B2G submissions to public administrations via FACe (submit, track, cancel). * **AEAT census** (`/v1/account/census-verification`, `/v1/clients/*`) — verify a NIF/CIF against the AEAT registry. * **Tax reports** (`/v1/tax_reports/*`) — generate, preview, download and keep history of Modelo 303 (VAT), 347 (annual operations with third parties) and 130 (IRPF instalment). ### Payments [#payments] * **Stripe auto-invoicing** (`/v1/stripe-autoinvoicing/*`) — connect Stripe accounts and issue invoices automatically from Stripe payments, including automatic corrective invoices on refunds. * **Payouts and reconciliation** (`/v1/payouts`, `/v1/connected-accounts`) — read Stripe payouts and reconcile settlements, with Norma 43 bank-statement support. ### Managed companies (gestorías) [#managed-companies-gestorías] * **Companies** (`/v1/companies`) — provision and run child companies from a master account: create, activate, deactivate, track creation status, and issue per-company API keys (create, rotate, revoke). Preview seat billing before you commit with `/v1/companies/seat-charge-preview`. Operate on a child's behalf on a single request with the `X-Active-Profile` header. ### Webhooks and events [#webhooks-and-events] * **Webhooks** (`/v1/webhook_endpoints` with nested `deliveries`) — subscribable endpoints signed with HMAC SHA256, dual-secret rotation, ping / test, and a delivery history you can replay. * **Events** (`/v1/events`, `/v1/event-catalog`) — the historical event stream and the catalog of subscribable event types. ### Account [#account] * **Account** (`/v1/account`) — introspect the authenticated credential (company, plan, scopes and rate-limit tier), manage API keys, personalize document templates, and run your own census verification. ## API foundations [#api-foundations] Behaviour every resource shares, so an integration learns it once: * **Test mode** — `fact_test_*` keys run against an isolated sandbox company; external effects (VeriFactu/AEAT, FACe, email, webhooks) are not executed, so you build and test without touching production data. * **Opaque identifiers** — every resource exposes an `id` whose value is a UUID v7, with foreign keys as `*_id`. * **Cursor pagination** — `starting_after` / `ending_before`, no `?page=`. * **Idempotency** — the `Idempotency-Key` header (max 64 chars, 24h TTL); a replay returns the original stored response — including a cached `4xx` — marked with `Idempotent-Replayed`. * **Rate limits** — per-tier per-minute and monthly quotas with `X-RateLimit-*` headers. * **Normalized errors** — the `{ error: { type, code, message, param, request_id, doc_url } }` envelope; validation errors point at the offending field through `param`. Branch on `code`, never on the human-facing `message`. * **Bulk operations** — batch endpoints report partial success per item, so one bad row doesn't fail the whole request. * **Import and export** — CSV client import (with a downloadable template) and Excel invoice export. * **Signed webhooks** — HMAC SHA256 with ±5min tolerance and exponential retries up to 8 attempts. * **Scopes** — a closed `resource:action` catalog; every operation you can't reach is hidden, and destructive `write` / `delete` scopes are flagged as sensitive on the OAuth consent screen and never pre-checked. * **Versioning** — the URL prefix `/v1` plus a pinned `Factuarea-Version` header. `/v1` stays stable for at least 24 months; any breaking change lives in `/v2` with a coexistence window of at least 12 months. ## Official SDKs — TypeScript & PHP [#official-sdks--typescript--php] Maintained SDKs wrap the full v1 REST API with a premium runtime, so you don't hand-roll HTTP. See the [SDKs section](/sdks). * **TypeScript / Node.js** — [`@factuarea/sdk`](https://www.npmjs.com/package/@factuarea/sdk) on npm. Dual ESM + CommonJS, full type declarations, Node 20+ (and Deno / Bun / Workers). Source: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). * **PHP** — [`factuarea/factuarea-php`](https://packagist.org/packages/factuarea/factuarea-php) on Packagist. PSR-4, Guzzle-based, PHP 8.2+. Source: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). ```bash npm install @factuarea/sdk composer require factuarea/factuarea-php ``` Both share the same runtime: automatic retries (with backoff, honouring `Retry-After`), automatic idempotency keys, cursor auto-pagination, a typed [error](/guides/errors) hierarchy, constant-time webhook verification, and binary (PDF) downloads. Every page in the API reference shows a ready-to-copy TypeScript, PHP and cURL snippet. Each release pins one [`Factuarea-Version`](/guides/versioning) and sends it on every request. ## Command-line interface [#command-line-interface] The official [`factuarea` CLI](/cli) (`v0.1.3`) drives the full v1 surface from your terminal. It is **agent-first** — stable JSON output, semantic exit codes and one-call discovery — and the command tree is generated from the OpenAPI spec, so it never drifts from the live surface. * **One key, two environments** — the key prefix selects the environment; a `fact_live_` mutation also requires the explicit `--live` flag as a safety net. * **Devloop** — `listen` forwards events to your machine and `trigger` produces real sandbox events, so you test webhooks locally with no tunnel or ngrok. * **Install** — Homebrew, npm or a `curl` installer. See the [CLI](/cli). ## MCP server for AI agents [#mcp-server-for-ai-agents] The [MCP server](/mcp) at `https://mcp.factuarea.com` exposes the public API as ** Model Context Protocol tools** over the **Streamable HTTP** transport, so AI agents (Claude and others) discover and call them without you wiring each endpoint. * **Two auth channels** — an **API key** (`fact_live_` / `fact_test_`) for the account owner (up to all tools), or **OAuth 2.1** for third-party apps (a curated -tool catalog). See [Connecting a client](/mcp/connect#authenticate). * **Full OAuth 2.1** — Dynamic Client Registration (RFC 7591), PKCE (S256), a consent screen with company **and** environment selection, refresh-token rotation with reuse detection, plus revocation and introspection. * **Scope-governed** — every tool enforces a fine-grained scope; tools you can't reach are hidden from `tools/list`. See [Scopes & permissions](/mcp/scopes). * **v1-faithful errors** — JSON-RPC errors preserve the same `code` and `http_status` as the REST API. See [Errors & rate limits](/mcp/errors). * **Claude Code** — the official `factuarea-mcp` [plugin](/mcp/claude-code-plugin) connects in two commands. * **Test mode** — drive everything against the isolated sandbox. See [Test mode](/mcp/connect#test-mode). ## Start in test mode [#start-in-test-mode] The golden rule across all four surfaces: **start in test mode**. Build against a `fact_test_` key (or an OAuth consent with the Test environment), then switch to `fact_live_` — no code changes. Welcome to the integrations era in Factuarea. --- # CLI overview (/cli) The official **`factuarea`** CLI drives the [v1 REST API](/api-reference/account/public-api.v1.account.show) from your terminal. It is **agent-first** — stable JSON output, semantic exit codes and one-call discovery — and Stripe-inspired: the full command tree is generated from the OpenAPI spec, so it never drifts from the live surface. The current stable release is **`v0.1.3`**. Prefer an AI agent to operate Factuarea directly? The CLI is built for it. See [Agents & scripting](/cli/agents) for the JSON contract and exit codes, and the [MCP server](/mcp) for the tool-based alternative. ## Install [#install] macOS and Linux: ```bash brew install --cask factuarea/tap/factuarea ``` Any platform with **Node 20 or newer**: ```bash npm i -g @factuarea/cli # or: npx @factuarea/cli ``` Installs a signed binary into `~/.local/bin`: ```bash curl -fsSL https://github.com/factuarea/factuarea-cli/releases/latest/download/install.sh | sh ``` Binaries are signed (cosign) and shipped with `checksums.txt` on [Releases](https://github.com/factuarea/factuarea-cli/releases). Requires **Go 1.26 or newer**: ```bash git clone https://github.com/factuarea/factuarea-cli && cd factuarea-cli make build # builds ./factuarea ``` macOS notarization and Windows Authenticode signing land in a later phase. For now on macOS use `brew` or `npm`, or run `xattr -d com.apple.quarantine ./factuarea` on a loose binary. ## Authenticate [#authenticate] The CLI uses your Factuarea **API key**. The key prefix selects the environment — there is no separate flag: * `fact_test_…` → the isolated [sandbox](/guides/test-mode): test data, no real effects (no AEAT transmission, no email, no webhook delivery). * `fact_live_…` → production: real data. **Log in** ```bash factuarea login # prompts for the key on a hidden prompt ``` The key is read on a hidden prompt — never passed as a visible argument. It is stored in the system keyring (falling back to `~/.config/factuarea/config.toml`, mode 600). Multiple **profiles** are supported with `--profile`. **Or set an environment variable** For non-interactive setups: ```bash export FACTUAREA_API_KEY=fact_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` **Verify** ```bash factuarea whoami # shows the account and the environment (TEST/LIVE) ``` Start every integration with a **`fact_test_`** key. The command surface is identical to production — swap the prefix to `fact_live_` only when your flow works end-to-end. Production mutations (with a `fact_live_` key) also require the explicit `--live` flag as a safety net. ## What's next [#whats-next] The generated command tree — list, show, create, domain actions, binary downloads, the `api` escape hatch and `commands --json`. Test webhooks locally without deploying or ngrok: `listen` forwards events to your machine, `trigger` produces real sandbox events. The agent-first contract: stable JSON on stdout, structured errors on stderr, semantic exit codes, scope-check and typed confirmation. --- # Agents & scripting (/cli/agents) The CLI is **agent-first**: an AI assistant or a script can discover the whole surface in one call, get stable machine-readable output, and branch on semantic exit codes instead of parsing prose. ## Discover the surface in one call [#discover-the-surface-in-one-call] ```bash factuarea commands --json ``` This dumps the full command manifest. Each entry carries: | Field | Meaning | | ---------------- | ---------------------------------------------------------- | | `path` | The command path, e.g. `invoices create`. | | `args` | Positional arguments (path params). | | `flags` | Available flags. | | `mutating` | Whether the command writes (needs `--live` in production). | | `binary` | Whether it returns a binary (PDF/ZIP/XML) instead of JSON. | | `paginated` | Whether the command supports cursor pagination. | | `required_scope` | The scope the API key must hold, e.g. `invoices:read`. | | `irreversible` | Whether the operation cannot be undone. | | `example` | A ready-to-adapt example invocation. | `required_scope` and `irreversible` come straight from the OpenAPI spec's `x-required-scope` and `x-irreversible` extensions, so the CLI and the [API reference](/api-reference/account/public-api.v1.account.show) agree by construction. ## Output contract [#output-contract] * `--json` emits the **raw API body** on **stdout**. * Errors go to **stderr** as structured JSON — the same [error envelope](/guides/errors) as the API: `error.{type,code,message,request_id,doc_url}`. * Keep stdout for data and stderr for diagnostics: pipe stdout to `jq`, log stderr. ## Exit codes [#exit-codes] Branch on the exit code, never on the message: | Code | Meaning | | ---- | -------------------------- | | `0` | OK | | `2` | Local usage / guard error | | `3` | Authentication failed | | `4` | Permission / missing scope | | `5` | Validation error | | `6` | Not found | | `7` | Rate limit | | `8` | Conflict / idempotency | | `9` | Server error | | `10` | Network / timeout | ## Local scope-check [#scope-check] Before a call, the CLI checks that your key holds the operation's `required_scope`. If it doesn't, the command **fails locally with exit `4`** and a clear message — no wasted round trip that the API would reject with `403` anyway. * The check only runs when the operation declares a scope and resolves the key's scopes lazily (one `GET /v1/account` per invocation at most, memoized). * A `*` scope on the key covers any operation. * `--skip-scope-check` downgrades the block to a warning and continues — useful if your cached scopes are stale. The API's real `403` is still the last line of defense. ## Irreversible operations [#irreversible-operations] Operations the spec marks `x-irreversible` (deletes, `void`, terminal conversions, fiscal emission, certificate rotation, GDPR forget…) ask for a typed confirmation before the call: ```bash factuarea invoices delete --confirm ``` * Pass `--confirm ` with the resource id to proceed. * In a non-interactive context (`--no-input` or no TTY) without `--confirm`, the command refuses with exit `2` rather than guessing. See the [scopes & irreversibility guide](/guides/scopes-and-irreversibility) for the full list of which operations carry each scope and which are irreversible. Want the agent to operate Factuarea through tools instead of CLI commands? Connect it to the [MCP server](/mcp) — the same surface exposed as tools, with OAuth and API-key auth. --- # Devloop (/cli/devloop) Test your webhooks locally without deploying or ngrok, Stripe-CLI style. The loop has two halves: **`listen`** forwards your account's events to your machine, **`trigger`** produces real sandbox events to forward. ## Forward events to localhost [#forward-events-to-localhost] ```bash factuarea listen --forward-to http://localhost:3000/webhooks ``` `listen` polls the event feed, rebuilds the webhook body and signs it with HMAC (`Factuarea-Signature`) using an ephemeral `whsec_…` secret it prints on start. Configure that secret in your verifier and your verification code runs unchanged — no code differences between local and production. For safety, `listen` only forwards to `localhost`. To forward to a remote host, pass `--allow-remote-forward` explicitly. ## Produce events to test against [#produce-events-to-test-against] In another terminal, produce real events in the sandbox: ```bash factuarea trigger invoice.paid factuarea trigger --list # supported events ``` `trigger` only operates in the **sandbox** — it requires a `fact_test_` key. It never produces events against production data. ## How verification stays identical [#how-verification-stays-identical] The signature scheme is the same one the platform uses, so the verifier you ship to production is the verifier you test with locally: * HMAC-SHA256 over the raw body with a constant-time comparison. * A timestamp tolerance that rejects replays. * Both signatures accepted during a secret-rotation grace window. The only difference is the secret: locally it is the ephemeral `whsec_…` from `listen`; in production it is the endpoint secret. See [Webhooks](/guides/webhooks) for the full signature contract and the SDK verifiers. A future phase replaces the polling in `listen` with a WebSocket relay. The command surface stays the same; only the transport changes. --- # Usage (/cli/usage) The command tree covers every resource in the API (`factuarea [] `), generated from the OpenAPI spec so it never drifts from the live surface. ## Reading data [#reading-data] ```bash # List (with automatic cursor pagination) factuarea invoices list --json factuarea clients list --paginate --json # Get one factuarea invoices show --json ``` `--json` emits the raw API body on **stdout**. `--paginate` walks every page for you, following `next_cursor` until `has_more` is false. See [Pagination](/guides/pagination) for the underlying cursor semantics. ## Writing data [#writing-data] Pass the JSON body with `-d` (inline) or `--data-file` (a path). The API computes totals — do not pre-round them. ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","lines":[…]}' ``` Every mutation gets an automatic `Idempotency-Key` so a retried request never double-creates a resource. See [Idempotency](/guides/idempotency). ## Domain actions [#domain-actions] State changes are **discrete actions**, not a generic status flag — mirroring the API's own design: ```bash factuarea invoices send factuarea invoices mark-paid ``` Some actions are **irreversible** (deletes, `void`, conversions, fiscal emission). The CLI asks you to confirm them before the call — see [Irreversible operations](/cli/agents#irreversible-operations) and the [scopes & irreversibility guide](/guides/scopes-and-irreversibility). ## Time tracking (control-horario) [#time-tracking-control-horario] The control-horario add-on adds workforce resources — employees, work schedules, time entries, absences, presence, holidays, monthly closes and the gestoría summary. Each command is generated from the spec and gated by its fine-grained scope (`employees:*`, `time_entries:*`, `absences:*`, `work_schedules:*`, `presence:read`, `holidays:read`, `payroll_exports:*`). ```bash # Clock an employee in and out — every entry is hash-chained (RD-ley 8/2019) factuarea time-entries clock-in -d '{"employee_id":"…","source":"web"}' factuarea time-entries clock-out -d '{"employee_id":"…","source":"web"}' # Request time off, then approve it factuarea absence-requests create \ -d '{"employee_id":"…","absence_type_id":"…","start_date":"2026-08-01","end_date":"2026-08-05"}' factuarea absence-requests approve # Live team presence right now factuarea presence live --json # Close a month's immutable register, then export it (ITSS RD-ley 8/2019) factuarea monthly-time-record-closes create -d '{"year":2026,"month":7}' factuarea monthly-time-record-closes export --format rdley_8_2019 --json ``` ## Binary downloads and uploads [#binary-downloads-and-uploads] PDF, ZIP and XML endpoints stream a binary you save with `-o`. Multipart uploads take the file with a `--file-` flag: ```bash # Download a PDF factuarea invoices pdf -o invoice.pdf # Upload a certificate (multipart) factuarea verifactu certificates upload \ -d '{"certificate_password":"…"}' --file-certificate_file cert.p12 ``` ## The `api` escape hatch [#the-api-escape-hatch] Any endpoint is reachable directly with `factuarea api `, even ones without a dedicated command yet: ```bash factuarea api get /v1/account --json factuarea api post /v1/invoices -d '{…}' ``` ## The command manifest [#the-command-manifest] `factuarea commands --json` dumps the **full manifest** of commands in one call — path, args, flags, whether each mutates, whether it is binary or paginated, its required scope, whether it is irreversible, and an example. An agent discovers the entire surface in a single call: ```bash factuarea commands --json ``` See [Agents & scripting](/cli/agents) for the manifest fields and the JSON contract. ## Embedded API reference [#embedded-api-reference] A quick API reference travels with the binary — searches stay on your machine: ```bash factuarea docs search invoice ``` `docs search` reads the **OpenAPI spec embedded in the binary** and answers "which command do I call?". It returns *operations* — command, summary, method and path — and never touches the network. ## Searching the published docs [#searching-the-published-docs] `docs list`, `docs grep` and `docs get` read the **published documentation** — the `llms-full` corpus of [docs.factuarea.com](https://docs.factuarea.com) — and answer "what do the docs say about this?". They return *pages and sections*, from the guides, the API reference and the error catalog: ```bash factuarea docs list # every page: factuarea docs list /guides # only the ones under that prefix factuarea docs grep "idempotency-key" # documentation sections that match factuarea docs get /guides/idempotency # the whole page, as Markdown ``` The corpus is downloaded **whole and once**, stored in the system cache directory (`~/Library/Caches/factuarea/docs/` on macOS, `~/.cache/factuarea/docs/` on Linux) and filtered locally. While the copy is younger than **15 minutes** no network request happens at all, so a session that chains `list`, `grep` and `get` downloads once. <Callout type="info"> **Your search term never leaves the machine.** The URL fetched is fixed and does not depend on what you type — there is no search server at the other end. None of the four `docs` subcommands reads or sends an API key. </Callout> | Option | What it does | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `--refresh` | Downloads again, ignoring a copy that is still valid | | `--lang` | Language of the guides: `en`, `es` or `ca` (default `en`, the source language). The API reference is not translated and always comes through | | `--json` | Stable output on stdout: `path`/`title` for `list`, `path`/`title`/`section`/`snippet` for `grep`, `path`/`title`/`markdown` for `get` | If the download fails and a cached copy exists — even an expired one — that copy is used, a warning goes to **stderr** so the JSON on stdout stays parseable, and the exit code is `0`. With no copy at all, the exit code is `10` (network). Set `FACTUAREA_DOCS_URL` to fetch the corpus from another origin. --- # Error codes by category (/errors) Every error `code` is stable across versions and has its own page with the cause and the action to take. Pick a category below, or open the full reference table. | Category | Codes | | ------------------------------------------------------ | ----- | | [Account](/errors/index-account) | 3 | | [Authentication](/errors/index-authentication) | 7 | | [Authorization](/errors/index-authorization) | 9 | | [Clients](/errors/index-clients) | 9 | | [Companies](/errors/index-companies) | 5 | | [Delivery Notes](/errors/index-delivery-notes) | 4 | | [Employees](/errors/index-employees) | 2 | | [Events](/errors/index-events) | 1 | | [Idempotency](/errors/index-idempotency) | 3 | | [Invoices](/errors/index-invoices) | 40 | | [Notifications](/errors/index-notifications) | 1 | | [Payments](/errors/index-payments) | 5 | | [Products](/errors/index-products) | 6 | | [Proformas](/errors/index-proformas) | 18 | | [Purchase Invoices](/errors/index-purchase-invoices) | 15 | | [Quotes](/errors/index-quotes) | 4 | | [Rate Limit](/errors/index-rate-limit) | 2 | | [Recurring Invoices](/errors/index-recurring-invoices) | 15 | | [Request](/errors/index-request) | 38 | | [Series](/errors/index-series) | 17 | | [Server](/errors/index-server) | 9 | | [Suppliers](/errors/index-suppliers) | 2 | | [Tax Reports](/errors/index-tax-reports) | 6 | | [Taxes](/errors/index-taxes) | 26 | | [VeriFactu](/errors/index-verifactu) | 25 | | [Webhooks](/errors/index-webhooks) | 13 | ## Related [#related] * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # account_not_found (/errors/account_not_found) | Code | Type | HTTP | Category | | ------------------- | ----------------- | ---- | -------------------------------- | | `account_not_found` | `not_found_error` | 404 | [Account](/errors/index-account) | ## Cause [#cause] The account behind the key could not be resolved, which usually means the key no longer points at a live company. ## What to do [#what-to-do] Check the key belongs to an active company and reissue it if the company changed. ## Related [#related] * [All Account error codes](/errors/index-account) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # addon_not_active (/errors/addon_not_active) | Code | Type | HTTP | Category | | ------------------ | --------------------- | ---- | -------------------------------------------- | | `addon_not_active` | `authorization_error` | 403 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The functionality belongs to an add-on that is not active for the company right now. ## What to do [#what-to-do] Activate or renew the add-on; unlike a scope problem, no key gives access to a feature that is not subscribed. ## Message returned by the API [#message-returned-by-the-api] > Your current plan does not include public API access. Purchase or renew a Factuarea plan to use the API. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # addon_required (/errors/addon_required) | Code | Type | HTTP | Category | | ---------------- | ------------------------ | ---- | ---------------------------------- | | `addon_required` | `payment_required_error` | 402 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] Creating webhook endpoints belongs to the Developer API add-on, and the company does not have it active — the free tier allows zero endpoints. ## What to do [#what-to-do] Subscribe to the add-on and repeat the call; unlike a permission problem, what is missing here is the subscription, not the scope. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # alta_record_not_found (/errors/alta_record_not_found) | Code | Type | HTTP | Category | | ----------------------- | ----------------- | ---- | ------------------------------------ | | `alta_record_not_found` | `not_found_error` | 404 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The invoice has no registration record, so the operation that depends on it has nothing to work with. ## What to do [#what-to-do] Check that the invoice was issued with VeriFactu active; if the record was deferred by a certificate problem, fix the certificate and it gets created. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # alternative_id_type_invalid (/errors/alternative_id_type_invalid) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | -------------------------------- | | `alternative_id_type_invalid` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] The alternative identifier type is outside the catalogue `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. ## What to do [#what-to-do] Send the type matching the document you are recording; it is reported to AEAT alongside the identifier. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # anulacion_record_already_exists (/errors/anulacion_record_already_exists) | Code | Type | HTTP | Category | | --------------------------------- | ---------------- | ---- | ------------------------------------ | | `anulacion_record_already_exists` | `conflict_error` | 409 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The invoice already carries an annulment record in the chain, and annulment is reported only once. ## What to do [#what-to-do] Read the existing record to check its AEAT status instead of annulling again. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # api_key_already_revoked (/errors/api_key_already_revoked) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `api_key_already_revoked` | `invalid_request_error` | 422 | [Account](/errors/index-account) | ## Cause [#cause] The key was already revoked, and a revoked key admits no further operations: revocation is terminal. ## What to do [#what-to-do] Issue a new key if you need credentials again; there is nothing left to revoke or rotate on this one. ## Related [#related] * [All Account error codes](/errors/index-account) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # api_key_expired (/errors/api_key_expired) | Code | Type | HTTP | Category | | ----------------- | ---------------------- | ---- | ---------------------------------------------- | | `api_key_expired` | `authentication_error` | 401 | [Authentication](/errors/index-authentication) | ## Cause [#cause] The key passed its expiry date. ## What to do [#what-to-do] Issue a new key; if you set expiry dates, plan the rotation before the date so the integration never goes dark. ## Message returned by the API [#message-returned-by-the-api] > This API key has expired. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # api_key_not_found (/errors/api_key_not_found) | Code | Type | HTTP | Category | | ------------------- | ----------------- | ---- | -------------------------------- | | `api_key_not_found` | `not_found_error` | 404 | [Account](/errors/index-account) | ## Cause [#cause] The identifier does not match any API key of the authenticated company. ## What to do [#what-to-do] List your keys and use the `id` returned there; the secret of a key is never a valid identifier. ## Related [#related] * [All Account error codes](/errors/index-account) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # api_key_revoked (/errors/api_key_revoked) | Code | Type | HTTP | Category | | ----------------- | ---------------------- | ---- | ---------------------------------------------- | | `api_key_revoked` | `authentication_error` | 401 | [Authentication](/errors/index-authentication) | ## Cause [#cause] The key was revoked, and a revoked key never authenticates again — revocation is the way to cut off a leaked credential. ## What to do [#what-to-do] Issue a new key and roll it out wherever the old one was in use. ## Message returned by the API [#message-returned-by-the-api] > This API key has been revoked. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # api_version_invalid_format (/errors/api_version_invalid_format) | Code | Type | HTTP | Category | | ---------------------------- | ----------------------- | ---- | ---------------------------------- | | `api_version_invalid_format` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The payload version of the endpoint is not a `YYYY-MM-DD` date. ## What to do [#what-to-do] Send the version as a date, matching one of the published payload versions. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # api_version_unsupported (/errors/api_version_unsupported) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `api_version_unsupported` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The payload version is well formed but is not among the ones the platform serves. ## What to do [#what-to-do] Pick a supported version, or leave the field out to receive events in the current one. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # attachment_invalid_filename (/errors/attachment_invalid_filename) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `attachment_invalid_filename` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The file name is not usable: it is empty, it carries path components, or it exceeds 200 characters. ## What to do [#what-to-do] Send a plain file name with its extension, with no directories and no `../` segments. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # attachment_mime_not_allowed (/errors/attachment_mime_not_allowed) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `attachment_mime_not_allowed` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The file type is outside the accepted set: PDF, PNG, JPEG, XML and HTML. ## What to do [#what-to-do] Convert the document to PDF or send the original the supplier issued; spreadsheets and office documents are not accepted as fiscal attachments. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # attachment_missing (/errors/attachment_missing) | Code | Type | HTTP | Category | | -------------------- | ----------------- | ---- | ---------------------------------------------------- | | `attachment_missing` | `not_found_error` | 404 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The purchase invoice exists but carries no attached file, so there is nothing to download. ## What to do [#what-to-do] Upload the supplier document to the invoice before requesting the file. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # attachment_too_large (/errors/attachment_too_large) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `attachment_too_large` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The file exceeds the maximum size allowed for a document attachment. ## What to do [#what-to-do] Compress the PDF or lower the resolution of the scan before uploading it. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # business_rule_violation (/errors/business_rule_violation) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `business_rule_violation` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] A domain invariant rejected the operation. This code carries the family; `error.subcode` names the concrete rule and `error.message` explains it. ## What to do [#what-to-do] Look up `error.subcode` in the error reference: the payload may be perfectly valid and the operation still not allowed in the current state. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # cannot_archive_last_default_series (/errors/cannot_archive_last_default_series) | Code | Type | HTTP | Category | | ------------------------------------ | ----------------------- | ---- | ------------------------------ | | `cannot_archive_last_default_series` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The series is the only active one for its document type. Archiving it would leave the company with no numbering available and freeze that kind of document. ## What to do [#what-to-do] Create another series of the same type, make it the default, and archive this one afterwards. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # cannot_attach_to_cancelled_purchase_invoice (/errors/cannot_attach_to_cancelled_purchase_invoice) | Code | Type | HTTP | Category | | --------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `cannot_attach_to_cancelled_purchase_invoice` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The invoice is cancelled, and attaching documents to a cancelled record would alter closed documentation. ## What to do [#what-to-do] Register the expense again on a live invoice and attach the file there. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # cannot_have_both_tax_id_and_alternative_id (/errors/cannot_have_both_tax_id_and_alternative_id) | Code | Type | HTTP | Category | | -------------------------------------------- | ----------------------- | ---- | -------------------------------- | | `cannot_have_both_tax_id_and_alternative_id` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] The client sends `tax_id` and an alternative identifier at the same time. Fiscal identity is one: the alternative identifier exists precisely for parties without a Spanish tax id. ## What to do [#what-to-do] Keep `tax_id` for Spanish parties, or the alternative identifier with its type for foreign ones, and clear the other field. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # census_requires_tax_id (/errors/census_requires_tax_id) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `census_requires_tax_id` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] Census verification checks the pair name plus tax id against AEAT, and one of the two is missing. ## What to do [#what-to-do] Fill in the tax id of the party being verified before requesting the check. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # certificate_expired (/errors/certificate_expired) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------------ | | `certificate_expired` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The certificate is outside its validity window: it has expired, or it is not valid yet. ## What to do [#what-to-do] Renew the certificate with FNMT and upload the new one; uploading a valid certificate re-queues the records left pending. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # certificate_nif_mismatch (/errors/certificate_nif_mismatch) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | ------------------------------------ | | `certificate_nif_mismatch` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The tax id of the certificate holder does not match the company tax id. AEAT records are signed on behalf of the company, so both must be the same. ## What to do [#what-to-do] Upload the certificate issued for this company tax id, or correct `tax_id` on the company if that is what is wrong. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # certificate_not_found (/errors/certificate_not_found) | Code | Type | HTTP | Category | | ----------------------- | ----------------- | ---- | ------------------------------------ | | `certificate_not_found` | `not_found_error` | 404 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The company has no FNMT certificate matching the identifier, or none uploaded at all. ## What to do [#what-to-do] Upload the `.p12` certificate of the company; without it no record can be signed or transmitted. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # certificate_too_large (/errors/certificate_too_large) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ------------------------------------ | | `certificate_too_large` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The file exceeds the 100 KB limit, while a real FNMT certificate weighs a few kilobytes. ## What to do [#what-to-do] Make sure you are uploading the certificate itself and not a bundle, an archive or a backup that contains it. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # client_has_documents (/errors/client_has_documents) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | -------------------------------- | | `client_has_documents` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] The client is referenced by issued documents. Deleting it would leave invoices, quotes or delivery notes without the party they were issued to, and fiscal records must remain traceable. ## What to do [#what-to-do] Deactivate the client instead of deleting it: it stops appearing in selectors and its documents keep their reference. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # client_import_too_large (/errors/client_import_too_large) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `client_import_too_large` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] The CSV exceeds the row limit the synchronous import accepts, since the whole file is processed within the request. ## What to do [#what-to-do] Split the file into smaller batches and import them one after another. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # client_not_found (/errors/client_not_found) | Code | Type | HTTP | Category | | ------------------ | ----------------- | ---- | -------------------------------- | | `client_not_found` | `not_found_error` | 404 | [Clients](/errors/index-clients) | ## Cause [#cause] The identifier does not resolve to any client of the authenticated company. ## What to do [#what-to-do] Check the `id` and the active profile, or find the client by `tax_id` or by `external_id` before creating a duplicate. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # client_requires_tax_identity (/errors/client_requires_tax_identity) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | -------------------------------- | | `client_requires_tax_identity` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] The client carries no fiscal identity: neither `tax_id` nor an alternative identifier, and an invoice cannot be issued to an unidentified party. ## What to do [#what-to-do] Fill in `tax_id`, or an alternative identifier with its type when the client has no Spanish tax id. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # clock_drift_exceeded (/errors/clock_drift_exceeded) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ------------------------------------ | | `clock_drift_exceeded` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The server clock drifted from NTP beyond the allowed margin. The generation timestamp is part of the AEAT fingerprint, so an unsynchronised clock would produce records AEAT rejects. ## What to do [#what-to-do] This is a server-side condition, not a payload problem: retry in a few minutes and, if it persists, report the `request_id` to support. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # company_inactive (/errors/company_inactive) | Code | Type | HTTP | Category | | ------------------ | --------------------- | ---- | ------------------------------------ | | `company_inactive` | `authorization_error` | 403 | [Companies](/errors/index-companies) | ## Cause [#cause] The profile named in `X-Active-Profile` is one of your managed companies, but it is deactivated and cannot be operated until it comes back. ## What to do [#what-to-do] Reactivate the managed company, or point the header at another profile. ## Related [#related] * [All Companies error codes](/errors/index-companies) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # conflicting_pagination_params (/errors/conflicting_pagination_params) | Code | Type | HTTP | Category | | ------------------------------- | ----------------------- | ---- | -------------------------------- | | `conflicting_pagination_params` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] `starting_after` and `ending_before` travelled in the same request. They walk the collection in opposite directions, so only one of them can apply. ## What to do [#what-to-do] Keep a single cursor: `starting_after` to move forward through the collection, `ending_before` to move backwards. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # corrective_invoice_inanulable (/errors/corrective_invoice_inanulable) | Code | Type | HTTP | Category | | ------------------------------- | ----------------------- | ---- | ---------------------------------- | | `corrective_invoice_inanulable` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice is itself a corrective, and correctives are never annulled: the correction chain has to stay auditable end to end. ## What to do [#what-to-do] Issue a new corrective invoice against the original one, carrying the right amounts. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # custom_header_blocklisted (/errors/custom_header_blocklisted) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ---------------------------------- | | `custom_header_blocklisted` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] One of the custom headers is reserved: the HTTP layer manages it (`host`, `content-type`, `content-length`, `user-agent`), Factuarea sends it as part of the signed contract (`factuarea-*`), or the proxy owns it (`x-forwarded-*`). ## What to do [#what-to-do] Rename the header — `x-my-app-token` instead of a reserved one — or drop it if the platform already sends the information. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # custom_header_value_too_long (/errors/custom_header_value_too_long) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ---------------------------------- | | `custom_header_value_too_long` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The value of a custom header exceeds 1024 characters. ## What to do [#what-to-do] Send a short token or reference instead of the full payload; the event body is the place for data. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # custom_tax_creation_disabled (/errors/custom_tax_creation_disabled) | Code | Type | HTTP | Category | | ------------------------------ | --------------------- | ---- | ---------------------------- | | `custom_tax_creation_disabled` | `authorization_error` | 403 | [Taxes](/errors/index-taxes) | ## Cause [#cause] Creating custom taxes is disabled for this company. ## What to do [#what-to-do] Use a tax of the canonical catalogue, and set your preferences through the company tax defaults. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # declaracion_already_exists (/errors/declaracion_already_exists) | Code | Type | HTTP | Category | | ---------------------------- | ---------------- | ---- | ------------------------------------ | | `declaracion_already_exists` | `conflict_error` | 409 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The company already filed its SIF responsibility statement for that period. ## What to do [#what-to-do] Download the existing statement instead of generating a new one. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # declaracion_not_found (/errors/declaracion_not_found) | Code | Type | HTTP | Category | | ----------------------- | ----------------- | ---- | ------------------------------------ | | `declaracion_not_found` | `not_found_error` | 404 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The company has no SIF responsibility statement filed for the requested period. ## What to do [#what-to-do] Generate the statement before downloading or querying it. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # delivery_note_not_found (/errors/delivery_note_not_found) | Code | Type | HTTP | Category | | ------------------------- | ----------------- | ---- | ---------------------------------------------- | | `delivery_note_not_found` | `not_found_error` | 404 | [Delivery Notes](/errors/index-delivery-notes) | ## Cause [#cause] The identifier does not resolve to any delivery note of the authenticated company. ## What to do [#what-to-do] Check the `id` and the active profile, or look the delivery note up by its `external_id`. ## Related [#related] * [All Delivery Notes error codes](/errors/index-delivery-notes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # delivery_note_section_not_editable_in_status (/errors/delivery_note_section_not_editable_in_status) | Code | Type | HTTP | Category | | ---------------------------------------------- | ----------------------- | ---- | ---------------------------------------------- | | `delivery_note_section_not_editable_in_status` | `invalid_request_error` | 422 | [Delivery Notes](/errors/index-delivery-notes) | ## Cause [#cause] The logistics section — carrier, vehicle, driver — is frozen because the delivery note is already delivered, invoiced or cancelled. ## What to do [#what-to-do] Record the correction on the invoice that bills the delivery, or issue a new delivery note if the goods travel again. ## Related [#related] * [All Delivery Notes error codes](/errors/index-delivery-notes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # dependency_unavailable (/errors/dependency_unavailable) | Code | Type | HTTP | Category | | ------------------------ | --------------------------- | ---- | ------------------------------ | | `dependency_unavailable` | `service_unavailable_error` | 503 | [Server](/errors/index-server) | ## Cause [#cause] An external service the operation relies on did not answer in time. ## What to do [#what-to-do] Retry after a short back-off; if the operation is a write, reuse the same `Idempotency-Key` so the retry cannot duplicate it. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # direct_debit_requires_default_bank_account (/errors/direct_debit_requires_default_bank_account) | Code | Type | HTTP | Category | | -------------------------------------------- | ----------------------- | ---- | -------------------------------- | | `direct_debit_requires_default_bank_account` | `invalid_request_error` | 422 | [Clients](/errors/index-clients) | ## Cause [#cause] Direct debit was selected as the payment method, but the client has no default bank account to charge. ## What to do [#what-to-do] Add a bank account to the client and mark it as default, then set the payment method. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # document_type_required_for_ambiguous_code (/errors/document_type_required_for_ambiguous_code) | Code | Type | HTTP | Category | | ------------------------------------------- | ----------------------- | ---- | ------------------------------ | | `document_type_required_for_ambiguous_code` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] That series code exists for more than one document type, so on its own it does not identify a single series. ## What to do [#what-to-do] Repeat the lookup adding the document type alongside the code. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # driver_tax_id_requires_name (/errors/driver_tax_id_requires_name) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------------------------- | | `driver_tax_id_requires_name` | `invalid_request_error` | 422 | [Delivery Notes](/errors/index-delivery-notes) | ## Cause [#cause] The driver tax id was sent without the driver name, and an identifier with no name identifies nobody on the delivery document. ## What to do [#what-to-do] Send `driver_name` together with `driver_tax_id`, or leave both out. ## Related [#related] * [All Delivery Notes error codes](/errors/index-delivery-notes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # duplicate_tax_default_for_document_type (/errors/duplicate_tax_default_for_document_type) | Code | Type | HTTP | Category | | ----------------------------------------- | ----------------------- | ---- | ---------------------------- | | `duplicate_tax_default_for_document_type` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] Another tax of the same type is already the default for that document type, and the pair (tax type, document type) admits a single default. ## What to do [#what-to-do] Clear the default on the tax that currently holds it, or set the new default on a different document type. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # employee_seat_charge_failed (/errors/employee_seat_charge_failed) | Code | Type | HTTP | Category | | ----------------------------- | ------------------------ | ---- | ------------------------------------ | | `employee_seat_charge_failed` | `payment_required_error` | 402 | [Employees](/errors/index-employees) | ## Cause [#cause] The immediate pro-rated charge for the employee seat was declined: the card was refused, it needs authentication, or the payment provider was unreachable. The employee is not activated if the seat is not paid. ## What to do [#what-to-do] Fix the payment method in the billing portal and retry; check with your bank if the card keeps being declined. ## Related [#related] * [All Employees error codes](/errors/index-employees) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # employee_seat_payment_method_required (/errors/employee_seat_payment_method_required) | Code | Type | HTTP | Category | | --------------------------------------- | ------------------------ | ---- | ------------------------------------ | | `employee_seat_payment_method_required` | `payment_required_error` | 402 | [Employees](/errors/index-employees) | ## Cause [#cause] Adding or reactivating an employee charges a seat immediately, and the company operates in live mode with no payment method on file. ## What to do [#what-to-do] Open the billing portal at `error.details.payment_setup_url`, register a payment method and repeat the same call. ## Related [#related] * [All Employees error codes](/errors/index-employees) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # event_already_processed (/errors/event_already_processed) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ------------------------------------ | | `event_already_processed` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] That SIF event is already recorded in the event chain, and each event is processed exactly once. ## What to do [#what-to-do] Do not send the event again; the existing entry already covers it. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # event_not_found (/errors/event_not_found) | Code | Type | HTTP | Category | | ----------------- | ----------------- | ---- | ------------------------------ | | `event_not_found` | `not_found_error` | 404 | [Events](/errors/index-events) | ## Cause [#cause] The identifier does not match any event of the authenticated company, or the event was purged by the 30-day retention policy. ## What to do [#what-to-do] Read the current state from the resource the event referred to; the event feed is a recent window, not a permanent archive. ## Related [#related] * [All Events error codes](/errors/index-events) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # export_limit_exceeded (/errors/export_limit_exceeded) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ---------------------------------- | | `export_limit_exceeded` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The filtered selection exceeds the 5,000-invoice cap of the export, so the file is refused up front instead of being silently truncated. ## What to do [#what-to-do] Narrow the filters — by date range or series — and export the invoices in several batches. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # external_id_already_exists (/errors/external_id_already_exists) | Code | Type | HTTP | Category | | ---------------------------- | ---------------- | ---- | -------------------------------- | | `external_id_already_exists` | `conflict_error` | 409 | [Request](/errors/index-request) | ## Cause [#cause] The `external_id` you use to reconcile with your own system is already assigned to another object of the same type in this company. ## What to do [#what-to-do] Look the object up by its `external_id` and update it, or assign a different value: `external_id` is unique per resource type and company. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # face_transmission_failed (/errors/face_transmission_failed) | Code | Type | HTTP | Category | | -------------------------- | ----------- | ---- | ------------------------------ | | `face_transmission_failed` | `api_error` | 502 | [Server](/errors/index-server) | ## Cause [#cause] The FACe platform — the public administration entry point — was unreachable or answered with a fault. The failure is upstream, not in your request. ## What to do [#what-to-do] Retry later; the invoice keeps its state and can be submitted again without being re-issued. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # facturae_signing_failed (/errors/facturae_signing_failed) | Code | Type | HTTP | Category | | ------------------------- | ----------- | ---- | ------------------------------ | | `facturae_signing_failed` | `api_error` | 500 | [Server](/errors/index-server) | ## Cause [#cause] The XAdES signature of the Facturae file could not be produced, usually because the signing certificate is unusable at that moment. ## What to do [#what-to-do] Check the company certificate is valid and matches its tax id; once fixed, generate the file again. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # feature_not_available_in_plan (/errors/feature_not_available_in_plan) | Code | Type | HTTP | Category | | ------------------------------- | --------------------- | ---- | -------------------------------------------- | | `feature_not_available_in_plan` | `authorization_error` | 403 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The feature is not included in the company plan. ## What to do [#what-to-do] Upgrade the plan to one that includes it, or use the equivalent feature your current plan does offer. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # forbidden_action (/errors/forbidden_action) | Code | Type | HTTP | Category | | ------------------ | --------------------- | ---- | -------------------------------------------- | | `forbidden_action` | `authorization_error` | 403 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The action is blocked for this resource even though the scope is right: the resource belongs to a shared catalogue, or the change travels through a different endpoint. ## What to do [#what-to-do] Read `error.subcode` and `error.message`: they name the canonical route for what you are trying to do. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # gestoria_module_required (/errors/gestoria_module_required) | Code | Type | HTTP | Category | | -------------------------- | --------------------- | ---- | ------------------------------------ | | `gestoria_module_required` | `authorization_error` | 403 | [Companies](/errors/index-companies) | ## Cause [#cause] The master company holds a live plan, but one without the accounting-firm module, so it cannot create or operate managed companies. ## What to do [#what-to-do] Upgrade to a plan that includes the module; this is a plan limit, not a missing payment. ## Related [#related] * [All Companies error codes](/errors/index-companies) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # gestoria_plan_required (/errors/gestoria_plan_required) | Code | Type | HTTP | Category | | ------------------------ | ------------------------ | ---- | ------------------------------------ | | `gestoria_plan_required` | `payment_required_error` | 402 | [Companies](/errors/index-companies) | ## Cause [#cause] The accounting firm has no active paid subscription, so there is no subscription on which to charge the seat. ## What to do [#what-to-do] Subscribe to a plan, or resume the cancelled one, before adding managed companies. ## Related [#related] * [All Companies error codes](/errors/index-companies) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # idempotency_key_in_use (/errors/idempotency_key_in_use) | Code | Type | HTTP | Category | | ------------------------ | ------------------- | ---- | ---------------------------------------- | | `idempotency_key_in_use` | `idempotency_error` | 409 | [Idempotency](/errors/index-idempotency) | ## Cause [#cause] Another request with the same `Idempotency-Key` is still in flight, and the result is not known yet. ## What to do [#what-to-do] Wait for the first request to answer and read its response; retry the same key after a short back-off if the connection dropped. ## Related [#related] * [All Idempotency error codes](/errors/index-idempotency) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # idempotency_key_invalid (/errors/idempotency_key_invalid) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------------------- | | `idempotency_key_invalid` | `invalid_request_error` | 400 | [Idempotency](/errors/index-idempotency) | ## Cause [#cause] The `Idempotency-Key` does not fit the accepted format: it must be 1 to 255 printable ASCII characters. ## What to do [#what-to-do] Generate the key as a UUID or a random string, and keep it stable across the retries of one operation. ## Related [#related] * [All Idempotency error codes](/errors/index-idempotency) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # idempotency_key_reused (/errors/idempotency_key_reused) | Code | Type | HTTP | Category | | ------------------------ | ------------------- | ---- | ---------------------------------------- | | `idempotency_key_reused` | `idempotency_error` | 409 | [Idempotency](/errors/index-idempotency) | ## Cause [#cause] That `Idempotency-Key` was already used with a different payload. The key identifies one specific operation, so reusing it for another would make replay meaningless. ## What to do [#what-to-do] Use a fresh key for each distinct operation, and reuse a key only to retry the exact same request. ## Related [#related] * [All Idempotency error codes](/errors/index-idempotency) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Account error codes (/errors/index-account) Error codes emitted by Account. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------- | | [`account_not_found`](/errors/account_not_found) | `not_found_error` | 404 | The account behind the key could not be resolved, which usually means the key no longer points at a live company. | | [`api_key_already_revoked`](/errors/api_key_already_revoked) | `invalid_request_error` | 422 | The key was already revoked, and a revoked key admits no further operations: revocation is terminal. | | [`api_key_not_found`](/errors/api_key_not_found) | `not_found_error` | 404 | The identifier does not match any API key of the authenticated company. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Authentication error codes (/errors/index-authentication) Error codes emitted by Authentication. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ---------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`api_key_expired`](/errors/api_key_expired) | `authentication_error` | 401 | The key passed its expiry date. | | [`api_key_revoked`](/errors/api_key_revoked) | `authentication_error` | 401 | The key was revoked, and a revoked key never authenticates again — revocation is the way to cut off a leaked credential. | | [`invalid_api_key`](/errors/invalid_api_key) | `authentication_error` | 401 | The key does not match any active key. It may be mistyped, truncated, or belong to a different environment — test keys and live keys are not interchangeable. | | [`ip_not_allowed`](/errors/ip_not_allowed) | `authentication_error` | 401 | The key restricts the addresses it accepts, and the request came from one outside that list. | | [`missing_api_key`](/errors/missing_api_key) | `authentication_error` | 401 | The request carries no credentials: neither the `Authorization` header nor `X-API-Key`. | | [`origin_not_allowed`](/errors/origin_not_allowed) | `authentication_error` | 401 | The request comes from a browser origin that the key does not accept. | | [`too_many_auth_failures`](/errors/too_many_auth_failures) | `authentication_error` | 429 | Too many failed authentication attempts arrived from the same address, so it is temporarily locked out to stop credential guessing. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Authorization error codes (/errors/index-authorization) Error codes emitted by Authorization. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------- | --------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_not_active`](/errors/addon_not_active) | `authorization_error` | 403 | The functionality belongs to an add-on that is not active for the company right now. | | [`feature_not_available_in_plan`](/errors/feature_not_available_in_plan) | `authorization_error` | 403 | The feature is not included in the company plan. | | [`forbidden_action`](/errors/forbidden_action) | `authorization_error` | 403 | The action is blocked for this resource even though the scope is right: the resource belongs to a shared catalogue, or the change travels through a different endpoint. | | [`insufficient_scope`](/errors/insufficient_scope) | `authorization_error` | 403 | The key authenticates correctly but does not carry the scope this operation requires. Scopes are granted when the key is issued and are not widened at call time. | | [`max_api_keys_exceeded`](/errors/max_api_keys_exceeded) | `authorization_error` | 422 | The company reached the number of API keys its plan allows. | | [`max_webhook_endpoints_exceeded`](/errors/max_webhook_endpoints_exceeded) | `authorization_error` | 422 | The company reached the number of webhook endpoints its add-on tier allows. | | [`module_not_available_in_sandbox`](/errors/module_not_available_in_sandbox) | `authorization_error` | 403 | The resource belongs to a module vetoed in test mode. Sandbox never touches AEAT, banks or real billing, so those modules stay out on purpose. | | [`scope_not_allowed_by_plan`](/errors/scope_not_allowed_by_plan) | `authorization_error` | 422 | One of the requested scopes belongs to a module that the plan does not include, so the key would be born with a permission that could never be exercised. | | [`scope_not_allowed_in_sandbox`](/errors/scope_not_allowed_in_sandbox) | `authorization_error` | 422 | A test key cannot be born with scopes of modules vetoed in sandbox. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Clients error codes (/errors/index-clients) Error codes emitted by Clients. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | -------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alternative_id_type_invalid`](/errors/alternative_id_type_invalid) | `invalid_request_error` | 422 | The alternative identifier type is outside the catalogue `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. | | [`cannot_have_both_tax_id_and_alternative_id`](/errors/cannot_have_both_tax_id_and_alternative_id) | `invalid_request_error` | 422 | The client sends `tax_id` and an alternative identifier at the same time. Fiscal identity is one: the alternative identifier exists precisely for parties without a Spanish tax id. | | [`census_requires_tax_id`](/errors/census_requires_tax_id) | `invalid_request_error` | 422 | Census verification checks the pair name plus tax id against AEAT, and one of the two is missing. | | [`client_has_documents`](/errors/client_has_documents) | `invalid_request_error` | 422 | The client is referenced by issued documents. Deleting it would leave invoices, quotes or delivery notes without the party they were issued to, and fiscal records must remain traceable. | | [`client_import_too_large`](/errors/client_import_too_large) | `invalid_request_error` | 422 | The CSV exceeds the row limit the synchronous import accepts, since the whole file is processed within the request. | | [`client_not_found`](/errors/client_not_found) | `not_found_error` | 404 | The identifier does not resolve to any client of the authenticated company. | | [`client_requires_tax_identity`](/errors/client_requires_tax_identity) | `invalid_request_error` | 422 | The client carries no fiscal identity: neither `tax_id` nor an alternative identifier, and an invoice cannot be issued to an unidentified party. | | [`direct_debit_requires_default_bank_account`](/errors/direct_debit_requires_default_bank_account) | `invalid_request_error` | 422 | Direct debit was selected as the payment method, but the client has no default bank account to charge. | | [`tax_id_already_exists`](/errors/tax_id_already_exists) | `conflict_error` | 409 | Another client of the company already holds that tax id, and the tax id identifies the party uniquely inside a company. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Companies error codes (/errors/index-companies) Error codes emitted by Companies. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | -------------------------------------------------------------- | ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`company_inactive`](/errors/company_inactive) | `authorization_error` | 403 | The profile named in `X-Active-Profile` is one of your managed companies, but it is deactivated and cannot be operated until it comes back. | | [`gestoria_module_required`](/errors/gestoria_module_required) | `authorization_error` | 403 | The master company holds a live plan, but one without the accounting-firm module, so it cannot create or operate managed companies. | | [`gestoria_plan_required`](/errors/gestoria_plan_required) | `payment_required_error` | 402 | The accounting firm has no active paid subscription, so there is no subscription on which to charge the seat. | | [`payment_method_required`](/errors/payment_method_required) | `payment_required_error` | 402 | Adding a managed company charges a seat immediately, and the accounting firm operates in live mode with no payment method on file. | | [`seat_charge_failed`](/errors/seat_charge_failed) | `payment_required_error` | 402 | The immediate pro-rated charge for the seat was declined: the card was refused, it needs authentication, or the payment provider was unreachable. The company is not created if the seat is not paid. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Delivery Notes error codes (/errors/index-delivery-notes) Error codes emitted by Delivery Notes. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------- | | [`delivery_note_not_found`](/errors/delivery_note_not_found) | `not_found_error` | 404 | The identifier does not resolve to any delivery note of the authenticated company. | | [`delivery_note_section_not_editable_in_status`](/errors/delivery_note_section_not_editable_in_status) | `invalid_request_error` | 422 | The logistics section — carrier, vehicle, driver — is frozen because the delivery note is already delivered, invoiced or cancelled. | | [`driver_tax_id_requires_name`](/errors/driver_tax_id_requires_name) | `invalid_request_error` | 422 | The driver tax id was sent without the driver name, and an identifier with no name identifies nobody on the delivery document. | | [`signature_payload_too_large`](/errors/signature_payload_too_large) | `invalid_request_error` | 422 | The signature image exceeds the accepted size for the field. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Employees error codes (/errors/index-employees) Error codes emitted by Employees. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------------------- | ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`employee_seat_charge_failed`](/errors/employee_seat_charge_failed) | `payment_required_error` | 402 | The immediate pro-rated charge for the employee seat was declined: the card was refused, it needs authentication, or the payment provider was unreachable. The employee is not activated if the seat is not paid. | | [`employee_seat_payment_method_required`](/errors/employee_seat_payment_method_required) | `payment_required_error` | 402 | Adding or reactivating an employee charges a seat immediately, and the company operates in live mode with no payment method on file. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Events error codes (/errors/index-events) Error codes emitted by Events. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | -------------------------------------------- | ----------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------- | | [`event_not_found`](/errors/event_not_found) | `not_found_error` | 404 | The identifier does not match any event of the authenticated company, or the event was purged by the 30-day retention policy. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Idempotency error codes (/errors/index-idempotency) Error codes emitted by Idempotency. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------ | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`idempotency_key_in_use`](/errors/idempotency_key_in_use) | `idempotency_error` | 409 | Another request with the same `Idempotency-Key` is still in flight, and the result is not known yet. | | [`idempotency_key_invalid`](/errors/idempotency_key_invalid) | `invalid_request_error` | 400 | The `Idempotency-Key` does not fit the accepted format: it must be 1 to 255 printable ASCII characters. | | [`idempotency_key_reused`](/errors/idempotency_key_reused) | `idempotency_error` | 409 | That `Idempotency-Key` was already used with a different payload. The key identifies one specific operation, so reusing it for another would make replay meaningless. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Invoices error codes (/errors/index-invoices) Error codes emitted by Invoices. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | -------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`corrective_invoice_inanulable`](/errors/corrective_invoice_inanulable) | `invalid_request_error` | 422 | The invoice is itself a corrective, and correctives are never annulled: the correction chain has to stay auditable end to end. | | [`export_limit_exceeded`](/errors/export_limit_exceeded) | `invalid_request_error` | 422 | The filtered selection exceeds the 5,000-invoice cap of the export, so the file is refused up front instead of being silently truncated. | | [`invalid_correction_nature`](/errors/invalid_correction_nature) | `invalid_request_error` | 422 | `correction_nature` only accepts `S` (substitution: the corrective carries the full corrected amounts) or `I` (by difference: it carries only the delta). | | [`invalid_correction_reason`](/errors/invalid_correction_reason) | `invalid_request_error` | 422 | The correction reason is outside the closed fiscal list (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), which maps to the AEAT codes R1 to R4. | | [`invalid_invoice_id`](/errors/invalid_invoice_id) | `invalid_request_error` | 400 | The invoice reference received is not a valid identifier; it usually means an internal value slipped in where the API expects the public `id`. | | [`invalid_invoice_number`](/errors/invalid_invoice_number) | `invalid_request_error` | 422 | The invoice number does not follow the canonical format `SERIES-YYYY-NNN`, plus the `-RECn` suffix on correctives. | | [`invalid_invoice_status`](/errors/invalid_invoice_status) | `invalid_request_error` | 422 | The value sent as invoice status is outside the lifecycle catalogue (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). | | [`invalid_invoice_uuid`](/errors/invalid_invoice_uuid) | `invalid_request_error` | 400 | The invoice identifier in the path or in the payload is not a valid UUID. | | [`invalid_payment_method`](/errors/invalid_payment_method) | `invalid_request_error` | 422 | The payment method is outside the closed allowlist: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. | | [`invoice_already_annulled`](/errors/invoice_already_annulled) | `invalid_request_error` | 422 | The invoice was already annulled. Annulment is terminal and, with VeriFactu active, its annulment record has already reached AEAT. | | [`invoice_already_paid`](/errors/invoice_already_paid) | `invalid_request_error` | 422 | The invoice is already settled. `paid` is a terminal, accounting-closed state: the output VAT has been declared, or will be declared for the period. | | [`invoice_already_sent`](/errors/invoice_already_sent) | `invalid_request_error` | 422 | The invoice was already issued: it holds a definitive series number and, with VeriFactu active, its registration with AEAT. Issuing does not happen twice. | | [`invoice_cannot_assign_number`](/errors/invoice_cannot_assign_number) | `invalid_request_error` | 422 | A definitive number was requested for an invoice that is not a draft, or that already carries one. Series numbering is monotonic and numbers are never reassigned. | | [`invoice_invalid_status_transition`](/errors/invoice_invalid_status_transition) | `invalid_request_error` | 422 | The target status is unreachable from the current one. The lifecycle is directed: `draft` moves to `scheduled` or `sent`, `sent` to `paid`, `overdue` or `annulled`, and `paid`, `cancelled` and `annulled` are terminal. | | [`invoice_not_cancellable_in_current_state`](/errors/invoice_not_cancellable_in_current_state) | `invalid_request_error` | 422 | Cancelling withdraws a draft that is not yet fiscally binding, so it only applies while the invoice is `draft`. | | [`invoice_not_correctable_in_current_state`](/errors/invoice_not_correctable_in_current_state) | `invalid_request_error` | 422 | A corrective invoice can only be issued against an invoice that is already issued (`sent` or `paid`). A draft, a cancelled or an annulled invoice has nothing to correct. | | [`invoice_not_deletable_in_current_state`](/errors/invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Only `draft` and `cancelled` invoices can be deleted. A numbered invoice never disappears: the correlative sequence must stay auditable. | | [`invoice_not_editable_in_current_state`](/errors/invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Only a draft admits editing. Once issued, the invoice is immutable and its content is frozen along with its fiscal record. | | [`invoice_not_eligible_for_action`](/errors/invoice_not_eligible_for_action) | `invalid_request_error` | 422 | The requested action does not apply to this invoice: its type or its current state leaves it outside the scope of the operation. | | [`invoice_not_found`](/errors/invoice_not_found) | `not_found_error` | 404 | The identifier does not resolve to any invoice of the authenticated company. Invoices belonging to another company answer exactly the same way. | | [`invoice_not_modifiable_in_current_state`](/errors/invoice_not_modifiable_in_current_state) | `invalid_request_error` | 422 | The field you are changing is frozen for the current state — for instance the tax regime of an annulled invoice. | | [`invoice_not_paid`](/errors/invoice_not_paid) | `invalid_request_error` | 422 | A payment receipt was requested for an invoice with no settled payment, so there is nothing to certify. | | [`invoice_not_reschedulable_in_current_state`](/errors/invoice_not_reschedulable_in_current_state) | `invalid_request_error` | 422 | Rescheduling moves the issuing date of an invoice that is waiting in `scheduled`, and this invoice is not waiting. | | [`invoice_not_schedulable_in_current_state`](/errors/invoice_not_schedulable_in_current_state) | `invalid_request_error` | 422 | Only a draft can be scheduled: scheduling reserves a future issuing moment without consuming a series number yet. | | [`invoice_not_unschedulable_in_current_state`](/errors/invoice_not_unschedulable_in_current_state) | `invalid_request_error` | 422 | Unscheduling returns an invoice from `scheduled` to `draft`, so it only applies while it is still waiting to be issued. | | [`invoice_not_unsendable_in_current_state`](/errors/invoice_not_unsendable_in_current_state) | `invalid_request_error` | 422 | Undoing the delivery mark only applies to a `sent` invoice: it clears `sent_at` and keeps the invoice issued. | | [`invoice_requires_at_least_one_line`](/errors/invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | The invoice carries no operation line, so it has no taxable base and cannot be issued. This happens both when you send no lines at all and when every line you send is a disbursement: a disbursement is an amount paid on the customer's behalf (art. 78.Tres.3 LIVA), not an operation of your own. | | [`invoice_year_required_for_ambiguous_number`](/errors/invoice_year_required_for_ambiguous_number) | `invalid_request_error` | 422 | That invoice number exists in more than one fiscal year, so on its own it does not identify a single invoice. | | [`line_total_checksum_mismatch`](/errors/line_total_checksum_mismatch) | `invalid_request_error` | 422 | The `line_total` you declared does not match the one Factuarea computes for that line (quantity × price − discount + VAT − withholding + surcharge) and the deviation is above the one-cent tolerance. The amount that gets invoiced and reported to the tax authority is always the one computed here, so the discrepancy means your system and the issued invoice would not reconcile. | | [`line_type_invalid`](/errors/line_type_invalid) | `invalid_request_error` | 422 | The line type falls outside the closed `NORMAL` / `SUPLIDO` catalogue. An issued invoice only tells two natures apart: what you sell, which forms the taxable base and carries VAT, and a disbursement (`suplido`), money advanced in the name and on behalf of the customer, which is therefore left out of the base (art. 78.Tres.3 of the Spanish VAT Act). | | [`no_invoices_in_period`](/errors/no_invoices_in_period) | `invalid_request_error` | 422 | The quarterly operation found no invoices in the requested period, so there is nothing to package or send. | | [`payment_method_invalid`](/errors/payment_method_invalid) | `invalid_request_error` | 422 | Same closed allowlist as `invalid_payment_method`, reported when the value is rejected while reading the payment method field of the payload. | | [`reminder_not_applicable`](/errors/reminder_not_applicable) | `invalid_request_error` | 422 | The payment reminder does not apply: the invoice is not `sent` or `overdue`, there is no recipient email, the public link is missing or disabled, or another reminder went out in the last 24 hours. | | [`scheduled_for_in_past`](/errors/scheduled_for_in_past) | `invalid_request_error` | 422 | `scheduled_for` is not strictly in the future, so there is no waiting period to reserve. | | [`simplified_invoice_cannot_be_substituted`](/errors/simplified_invoice_cannot_be_substituted) | `invalid_request_error` | 422 | One invoice of the substitution list cannot be replaced: it is not simplified, it is cancelled or annulled, it belongs to another company, or it already has a substitute. | | [`simplified_invoice_not_allowed`](/errors/simplified_invoice_not_allowed) | `invalid_request_error` | 422 | The operation is not eligible for a simplified invoice: it exceeds EUR 3,000, or it is an intra-EU supply, an export, a reverse-charge operation, or the customer needs a full invoice to deduct VAT. | | [`simplified_limit_exceeded`](/errors/simplified_limit_exceeded) | `invalid_request_error` | 422 | The lines would push the simplified invoice (F2) over the absolute legal cap of EUR 3,000 VAT included. | | [`suplido_line_cannot_carry_taxes`](/errors/suplido_line_cannot_carry_taxes) | `invalid_request_error` | 422 | The disbursement line carries charges of its own: a VAT rate, withholding, equivalence surcharge, discount, regime key, exemption cause or product/pack. A disbursement is not an operation of the issuer, so charging tax on it would mean paying tax on a supply you never made, and tying it to a product would move stock you never sold. | | [`suplido_not_allowed_in_simplified_invoice`](/errors/suplido_not_allowed_in_simplified_invoice) | `invalid_request_error` | 422 | The invoice is simplified (F2) and a simplified invoice does not identify the recipient. With no identified recipient there is nobody to evidence the payment on behalf of, so the amount cannot take disbursement treatment on this invoice type. | | [`suplido_requires_source_invoice_reference`](/errors/suplido_requires_source_invoice_reference) | `invalid_request_error` | 422 | The disbursement line does not carry `source_invoice_reference`, the number of the supporting document the third party issued in the customer's name. Without that document the payment is not evidenced as made on someone else's behalf, and the tax authority would treat it as the issuer's own taxable base, with VAT charged on it. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Notifications error codes (/errors/index-notifications) Error codes emitted by Notifications. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------- | | [`notification_not_found`](/errors/notification_not_found) | `not_found_error` | 404 | The identifier does not match any notification of the authenticated company, or the notification fell out of the retention window. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Payments error codes (/errors/index-payments) Error codes emitted by Payments. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [`invalid_payment_date`](/errors/invalid_payment_date) | `invalid_request_error` | 422 | The payment date falls outside the accepted window: it cannot precede the invoice issue date, nor be in the future. | | [`payout_reconciliation_amount_mismatch`](/errors/payout_reconciliation_amount_mismatch) | `invalid_request_error` | 422 | The confirmed amount does not match the net amount of the payout, so the reconciliation would close with a difference nobody accounts for. | | [`receipt_not_available`](/errors/receipt_not_available) | `invalid_request_error` | 422 | There is no receipt to issue because the document has no settled payment behind it. | | [`stripe_payout_already_reconciled`](/errors/stripe_payout_already_reconciled) | `invalid_request_error` | 422 | The payout was already reconciled, and reconciliation is terminal: repeating it would double-count the bank entry. | | [`stripe_payout_not_found`](/errors/stripe_payout_not_found) | `not_found_error` | 404 | The identifier does not resolve to any payout of the authenticated company. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Products error codes (/errors/index-products) Error codes emitted by Products. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------- | | [`pack_in_use`](/errors/pack_in_use) | `invalid_request_error` | 422 | The pack is referenced by issued documents, so deleting it would break their composition. | | [`pack_not_found`](/errors/pack_not_found) | `not_found_error` | 404 | The identifier does not resolve to any pack of the authenticated company. | | [`pack_share_link_failed`](/errors/pack_share_link_failed) | `api_error` | 500 | The share link for the pack could not be produced. The pack itself is unaffected. | | [`product_in_use`](/errors/product_in_use) | `invalid_request_error` | 422 | The product is referenced by issued documents or by other catalogue entries, and removing it would leave those references dangling. | | [`product_not_found`](/errors/product_not_found) | `not_found_error` | 404 | The identifier does not resolve to any product of the authenticated company. | | [`sku_already_exists`](/errors/sku_already_exists) | `conflict_error` | 409 | Another product of the company already uses that SKU, and the SKU identifies the item uniquely in the catalogue. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Proformas error codes (/errors/index-proformas) Error codes emitted by Proformas. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_expiry_date`](/errors/invalid_expiry_date) | `invalid_request_error` | 422 | The expiry date is earlier than the issue date, or more than 365 days after it. | | [`invalid_proforma_id`](/errors/invalid_proforma_id) | `invalid_request_error` | 400 | The pro forma reference received is not a valid identifier, usually because an internal value replaced the public `id`. | | [`invalid_proforma_number`](/errors/invalid_proforma_number) | `invalid_request_error` | 422 | The pro forma number does not follow the canonical numbering format of its series. | | [`invalid_proforma_status`](/errors/invalid_proforma_status) | `invalid_request_error` | 422 | The value sent as status is outside the catalogue `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. | | [`invalid_proforma_uuid`](/errors/invalid_proforma_uuid) | `invalid_request_error` | 400 | The pro forma identifier in the path or in the payload is not a valid UUID. | | [`proforma_already_accepted`](/errors/proforma_already_accepted) | `invalid_request_error` | 422 | The customer already accepted the pro forma, and acceptance is registered once. | | [`proforma_already_rejected`](/errors/proforma_already_rejected) | `invalid_request_error` | 422 | The pro forma is already marked as rejected. | | [`proforma_cannot_be_accepted`](/errors/proforma_cannot_be_accepted) | `invalid_request_error` | 422 | Acceptance does not apply from the current state: an invoiced, cancelled or expired pro forma no longer admits it. | | [`proforma_cannot_be_rejected`](/errors/proforma_cannot_be_rejected) | `invalid_request_error` | 422 | Rejection does not apply from the current state: once invoiced, cancelled or expired, the pro forma is closed. | | [`proforma_cannot_be_sent`](/errors/proforma_cannot_be_sent) | `invalid_request_error` | 422 | Sending by email does not apply to a pro forma in a terminal state: there is no live offer to deliver. | | [`proforma_invalid_status_transition`](/errors/proforma_invalid_status_transition) | `invalid_request_error` | 422 | The target status is unreachable from the current one: a draft can be accepted, cancelled or expire; an accepted pro forma can be invoiced, rejected or expire; invoiced, cancelled and expired are terminal. | | [`proforma_not_convertible_in_current_state`](/errors/proforma_not_convertible_in_current_state) | `invalid_request_error` | 422 | Converting into an invoice requires the customer to have accepted the pro forma; from any other state there is no agreement to bill. | | [`proforma_not_deletable_in_current_state`](/errors/proforma_not_deletable_in_current_state) | `invalid_request_error` | 422 | Only a draft pro forma can be deleted. Once it has been accepted, rejected or invoiced, it is part of the commercial trail. | | [`proforma_not_draft`](/errors/proforma_not_draft) | `invalid_request_error` | 422 | The operation only makes sense while the pro forma is a draft, and this one has already moved on. | | [`proforma_not_editable_in_current_state`](/errors/proforma_not_editable_in_current_state) | `invalid_request_error` | 422 | Only a draft pro forma admits editing. Once it is accepted, rejected, expired, invoiced or cancelled, its content is settled. | | [`proforma_not_found`](/errors/proforma_not_found) | `not_found_error` | 404 | The identifier does not resolve to any pro forma of the authenticated company. | | [`proforma_requires_at_least_one_line`](/errors/proforma_requires_at_least_one_line) | `invalid_request_error` | 422 | The pro forma has no lines, so there is no amount to put in front of the customer. | | [`public_link_expires_at_exceeds_max_days`](/errors/public_link_expires_at_exceeds_max_days) | `invalid_request_error` | 422 | The requested expiry for the public link goes beyond the maximum window your plan allows for shared documents. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Purchase Invoices error codes (/errors/index-purchase-invoices) Error codes emitted by Purchase Invoices. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`attachment_invalid_filename`](/errors/attachment_invalid_filename) | `invalid_request_error` | 422 | The file name is not usable: it is empty, it carries path components, or it exceeds 200 characters. | | [`attachment_mime_not_allowed`](/errors/attachment_mime_not_allowed) | `invalid_request_error` | 422 | The file type is outside the accepted set: PDF, PNG, JPEG, XML and HTML. | | [`attachment_missing`](/errors/attachment_missing) | `not_found_error` | 404 | The purchase invoice exists but carries no attached file, so there is nothing to download. | | [`attachment_too_large`](/errors/attachment_too_large) | `invalid_request_error` | 422 | The file exceeds the maximum size allowed for a document attachment. | | [`cannot_attach_to_cancelled_purchase_invoice`](/errors/cannot_attach_to_cancelled_purchase_invoice) | `invalid_request_error` | 422 | The invoice is cancelled, and attaching documents to a cancelled record would alter closed documentation. | | [`invalid_purchase_invoice_id`](/errors/invalid_purchase_invoice_id) | `invalid_request_error` | 400 | The purchase invoice reference received is not a valid identifier, usually because an internal value replaced the public `id`. | | [`invalid_purchase_invoice_number`](/errors/invalid_purchase_invoice_number) | `invalid_request_error` | 422 | The invoice number is empty or does not fit the accepted format. On a purchase invoice the number is the one the supplier printed, not one Factuarea generates. | | [`invalid_purchase_invoice_uuid`](/errors/invalid_purchase_invoice_uuid) | `invalid_request_error` | 400 | The purchase invoice identifier in the path or in the payload is not a valid UUID. | | [`operation_regime_invalid`](/errors/operation_regime_invalid) | `invalid_request_error` | 422 | The operation regime is outside the catalogue `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. | | [`purchase_invoice_already_exists`](/errors/purchase_invoice_already_exists) | `conflict_error` | 409 | That supplier already has a purchase invoice registered with the same number. The pair supplier plus number identifies the document uniquely and prevents recording an expense twice. | | [`purchase_invoice_not_deletable_in_current_state`](/errors/purchase_invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Only draft and cancelled purchase invoices can be deleted. A pending or paid one is part of the expense ledger. | | [`purchase_invoice_not_draft`](/errors/purchase_invoice_not_draft) | `invalid_request_error` | 422 | The operation only applies while the purchase invoice is a draft, and this one has already been registered. | | [`purchase_invoice_not_editable_in_current_state`](/errors/purchase_invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Only a draft purchase invoice can be edited. Once registered as pending, paid or cancelled, its content backs an accounting entry. | | [`purchase_invoice_not_found`](/errors/purchase_invoice_not_found) | `not_found_error` | 404 | The identifier does not resolve to any purchase invoice of the authenticated company. | | [`purchase_invoice_requires_at_least_one_line`](/errors/purchase_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | The purchase invoice has no lines, so there is no expense nor deductible VAT to record. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Quotes error codes (/errors/index-quotes) Error codes emitted by Quotes. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------- | | [`quote_already_accepted`](/errors/quote_already_accepted) | `invalid_request_error` | 422 | The quote was already approved, and approval is registered once. | | [`quote_already_rejected`](/errors/quote_already_rejected) | `invalid_request_error` | 422 | The quote is already marked as rejected. | | [`quote_expired`](/errors/quote_expired) | `invalid_request_error` | 422 | The quote passed its validity date, so the offered conditions are no longer binding and it cannot be approved or converted as is. | | [`quote_not_found`](/errors/quote_not_found) | `not_found_error` | 404 | The identifier does not resolve to any quote of the authenticated company. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Rate Limit error codes (/errors/index-rate-limit) Error codes emitted by Rate Limit. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ------------------ | ---- | ---------------------------------------------------------------------- | | [`monthly_quota_exceeded`](/errors/monthly_quota_exceeded) | `rate_limit_error` | 429 | The company exhausted the monthly call quota its plan includes. | | [`rate_limit_exceeded`](/errors/rate_limit_exceeded) | `rate_limit_error` | 429 | The key sent more requests than its rate allows in the current window. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Recurring Invoices error codes (/errors/index-recurring-invoices) Error codes emitted by Recurring Invoices. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_frequency_interval`](/errors/invalid_frequency_interval) | `invalid_request_error` | 422 | The interval is lower than 1, so the recurrence would never advance to a next run. | | [`invalid_frequency_type`](/errors/invalid_frequency_type) | `invalid_request_error` | 422 | The frequency is outside the catalogue `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. | | [`invalid_holiday_handling`](/errors/invalid_holiday_handling) | `invalid_request_error` | 422 | The holiday policy is outside the catalogue `skip`, `before`, `after`, `same`. | | [`invalid_recurring_invoice_id`](/errors/invalid_recurring_invoice_id) | `invalid_request_error` | 400 | The recurrence reference received is not a valid identifier, usually because an internal value replaced the public `id`. | | [`invalid_recurring_invoice_uuid`](/errors/invalid_recurring_invoice_uuid) | `invalid_request_error` | 400 | The recurrence identifier in the path or in the payload is not a valid UUID. | | [`recurring_already_active`](/errors/recurring_already_active) | `invalid_request_error` | 422 | The recurrence is already running, so there is nothing to activate. Legacy code kept for compatibility: current endpoints report this as `recurring_invoice_already_active`. | | [`recurring_invoice_already_active`](/errors/recurring_invoice_already_active) | `invalid_request_error` | 422 | The recurrence is already running. | | [`recurring_invoice_already_cancelled`](/errors/recurring_invoice_already_cancelled) | `invalid_request_error` | 422 | The recurrence was already cancelled, and cancellation is terminal. | | [`recurring_invoice_already_paused`](/errors/recurring_invoice_already_paused) | `invalid_request_error` | 422 | The recurrence is already paused, so pausing it again changes nothing. | | [`recurring_invoice_cancelled_cannot_resume`](/errors/recurring_invoice_cancelled_cannot_resume) | `invalid_request_error` | 422 | A cancelled recurrence cannot be resumed: cancellation closes it for good, unlike a pause. | | [`recurring_invoice_cannot_run`](/errors/recurring_invoice_cannot_run) | `invalid_request_error` | 422 | The recurrence cannot generate an invoice right now: it is not running, its cycle is over, or it lacks the data an invoice needs. `error.message` states the specific reason. | | [`recurring_invoice_has_generated_invoices`](/errors/recurring_invoice_has_generated_invoices) | `invalid_request_error` | 422 | The recurrence already produced invoices, and those invoices depend on it for their traceability. | | [`recurring_invoice_not_found`](/errors/recurring_invoice_not_found) | `not_found_error` | 404 | The identifier does not resolve to any recurrence of the authenticated company. | | [`recurring_invoice_requires_at_least_one_line`](/errors/recurring_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | The recurrence has no lines, so every generated invoice would come out empty. | | [`recurring_not_active`](/errors/recurring_not_active) | `invalid_request_error` | 422 | The operation needs a running recurrence and this one is paused, completed or cancelled. Legacy code kept for compatibility with older integrations. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Request error codes (/errors/index-request) Error codes emitted by Request. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`business_rule_violation`](/errors/business_rule_violation) | `invalid_request_error` | 422 | A domain invariant rejected the operation. This code carries the family; `error.subcode` names the concrete rule and `error.message` explains it. | | [`conflicting_pagination_params`](/errors/conflicting_pagination_params) | `invalid_request_error` | 422 | `starting_after` and `ending_before` travelled in the same request. They walk the collection in opposite directions, so only one of them can apply. | | [`external_id_already_exists`](/errors/external_id_already_exists) | `conflict_error` | 409 | The `external_id` you use to reconcile with your own system is already assigned to another object of the same type in this company. | | [`invalid_param_format`](/errors/invalid_param_format) | `invalid_request_error` | 422 | A legacy form request rejected the shape of a value. Migrated endpoints report the same situation as `parameter_invalid_format` or `parameter_invalid_integer`. | | [`invalid_param_value`](/errors/invalid_param_value) | `invalid_request_error` | 422 | A legacy form request rejected the value of a field. Migrated endpoints report the same situation as `parameter_invalid_enum` or `parameter_invalid_range`. | | [`invalid_status_transition`](/errors/invalid_status_transition) | `invalid_request_error` | 422 | The requested state is not reachable from the state the document is in right now. | | [`length_required`](/errors/length_required) | `invalid_request_error` | 411 | A request with a body arrived using chunked transfer encoding, without declaring its size. The API needs the length up front to reject oversized payloads before buffering them. | | [`metadata_too_many_keys`](/errors/metadata_too_many_keys) | `invalid_request_error` | 422 | The `metadata` object exceeds the limit of 50 keys per resource. | | [`metadata_value_too_long`](/errors/metadata_value_too_long) | `invalid_request_error` | 422 | One value of `metadata` exceeds 500 characters once serialised to text. | | [`method_not_allowed`](/errors/method_not_allowed) | `invalid_request_error` | 405 | The path exists but does not accept the HTTP verb used. | | [`missing_required_param`](/errors/missing_required_param) | `invalid_request_error` | 422 | A legacy form request found a required field missing. Endpoints already migrated to the canonical parsers report the same situation as `parameter_missing`. | | [`parameter_invalid`](/errors/parameter_invalid) | `invalid_request_error` | 422 | A value object built from the payload rejected the value it received. `error.subcode` names which one — tax code, country code, rate, and so on. | | [`parameter_invalid_boolean`](/errors/parameter_invalid_boolean) | `invalid_request_error` | 400 | A parameter that must be a boolean received a value outside the accepted representations (`true`/`false`, `1`/`0`). | | [`parameter_invalid_cursor`](/errors/parameter_invalid_cursor) | `invalid_request_error` | 400 | The `starting_after` or `ending_before` cursor is not a valid UUID, so it cannot point at any row of the collection. | | [`parameter_invalid_empty`](/errors/parameter_invalid_empty) | `invalid_request_error` | 400 | A parameter arrived with an empty value: an `in` filter with no items, a comparison with nothing after the operator, or an equality filter with an empty string. | | [`parameter_invalid_enum`](/errors/parameter_invalid_enum) | `invalid_request_error` | 400 | The value falls outside the closed set the parameter accepts. On listings it also covers a filter operator other than `eq`, `gte`, `lte`, `gt`, `lt`, `in` or `contains`. | | [`parameter_invalid_format`](/errors/parameter_invalid_format) | `invalid_request_error` | 400 | The value has the right type but not the shape the parameter requires: a date, an identifier pattern or a header such as `Factuarea-Version`. | | [`parameter_invalid_integer`](/errors/parameter_invalid_integer) | `invalid_request_error` | 400 | A parameter that must be a whole number received something that cannot be parsed as one, such as `limit=abc`. | | [`parameter_invalid_iso8601`](/errors/parameter_invalid_iso8601) | `invalid_request_error` | 400 | A range filter (`gte`, `lte`, `gt`, `lt`) received a value that is neither numeric nor an ISO 8601 date. | | [`parameter_invalid_range`](/errors/parameter_invalid_range) | `invalid_request_error` | 400 | A numeric parameter fell outside its accepted bounds. The usual case is `limit`, which must be between 1 and 100. | | [`parameter_invalid_string`](/errors/parameter_invalid_string) | `invalid_request_error` | 400 | A parameter that must be text received an array, an object or a value that cannot be read as a string. | | [`parameter_invalid_url`](/errors/parameter_invalid_url) | `invalid_request_error` | 400 | A field that must hold an absolute URL received a value that is not one, usually because the scheme or the host is missing. | | [`parameter_invalid_uuid`](/errors/parameter_invalid_uuid) | `invalid_request_error` | 400 | An identifier field received a value that is not a valid UUID. Every v1 resource id is a UUID. | | [`parameter_invalid_value`](/errors/parameter_invalid_value) | `invalid_request_error` | 422 | The value is syntactically correct but not admissible for this resource: outside the canonical catalogue of the field, or inconsistent with the rest of the payload. | | [`parameter_missing`](/errors/parameter_missing) | `invalid_request_error` | 400 | The endpoint requires a parameter that the request did not carry. `error.param` names it. | | [`parameter_unknown`](/errors/parameter_unknown) | `invalid_request_error` | 400 | The request carries a parameter the endpoint does not accept: a filter outside its allowlist, a `sort` field that is not sortable, or the offset-style `page` — v1 paginates by cursor. | | [`payload_too_large`](/errors/payload_too_large) | `invalid_request_error` | 413 | The request body exceeds the accepted size: 1 MB as a rule, 6 MB on the endpoints that accept files. | | [`profile_not_found`](/errors/profile_not_found) | `not_found_error` | 404 | The `X-Active-Profile` header names a company that does not exist or does not belong to the accounting-firm tree of the authenticated key. Both cases answer the same so that the API never reveals companies of other tenants. | | [`resource_already_exists`](/errors/resource_already_exists) | `conflict_error` | 409 | Creating the object would duplicate one that already exists under a unique key — tax id, SKU, external id. `error.details.existing_resource_id` points at the object that already holds the value. | | [`resource_conflict`](/errors/resource_conflict) | `conflict_error` | 409 | The operation collided with the current state of the resource and no more specific conflict code applies. | | [`resource_immutable`](/errors/resource_immutable) | `invalid_request_error` | 422 | The object is closed to changes for this operation: its state or its accounting record forbids modifying it. | | [`resource_locked`](/errors/resource_locked) | `conflict_error` | 409 | Another operation holds the resource until it finishes: concurrent writes on the same object are serialised instead of interleaved. | | [`resource_not_deletable`](/errors/resource_not_deletable) | `invalid_request_error` | 422 | The object exists but its state or its dependants block the deletion. In bulk deletions this is the per-row code of every entry that could not be removed. | | [`resource_not_found`](/errors/resource_not_found) | `not_found_error` | 404 | The identifier resolves to nothing visible to the authenticated company. Objects belonging to another company answer exactly the same way, by design. | | [`route_not_found`](/errors/route_not_found) | `not_found_error` | 404 | The path does not match any v1 endpoint. It is usually a typo, a missing `/v1` prefix, or a path from a different area of the API. | | [`unknown_filter`](/errors/unknown_filter) | `invalid_request_error` | 422 | A listing received a filter it does not know. The canonical v1 parsers report this as `parameter_unknown`; this code survives for endpoints that have not migrated yet. | | [`unsupported_api_version`](/errors/unsupported_api_version) | `invalid_request_error` | 400 | The `Factuarea-Version` header is well formed but names a version outside the supported set. | | [`unsupported_media_type`](/errors/unsupported_media_type) | `invalid_request_error` | 415 | A request with a body declared a `Content-Type` other than `application/json`. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Series error codes (/errors/index-series) Error codes emitted by Series. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`cannot_archive_last_default_series`](/errors/cannot_archive_last_default_series) | `invalid_request_error` | 422 | The series is the only active one for its document type. Archiving it would leave the company with no numbering available and freeze that kind of document. | | [`document_type_required_for_ambiguous_code`](/errors/document_type_required_for_ambiguous_code) | `invalid_request_error` | 422 | That series code exists for more than one document type, so on its own it does not identify a single series. | | [`invalid_series_code`](/errors/invalid_series_code) | `invalid_request_error` | 422 | The series code is empty, too long, or carries characters that do not belong in a fiscal prefix. | | [`invalid_series_name`](/errors/invalid_series_name) | `invalid_request_error` | 422 | The series name is empty or exceeds the allowed length. | | [`invalid_series_number`](/errors/invalid_series_number) | `invalid_request_error` | 422 | The starting number is not valid: it is not a positive integer, or it falls at or below the last number already issued, which would re-issue numbers already in use. | | [`invalid_series_uuid`](/errors/invalid_series_uuid) | `invalid_request_error` | 400 | The series identifier in the path or in the payload is not a valid UUID. | | [`invalid_series_year`](/errors/invalid_series_year) | `invalid_request_error` | 422 | The fiscal year is not a valid four-digit year for a numbering series. | | [`monthly_requires_month_segmented_format`](/errors/monthly_requires_month_segmented_format) | `invalid_request_error` | 422 | The counter resets monthly but the numbering mask does not segment by month, so two months would start on the same correlative and produce duplicate numbers within the year. | | [`series_already_archived`](/errors/series_already_archived) | `invalid_request_error` | 422 | The series was already archived, and archiving is not repeated: a second call means the client is out of sync with the real state. | | [`series_code_immutable_with_documents`](/errors/series_code_immutable_with_documents) | `invalid_request_error` | 422 | Changing the prefix of a series that already issued documents would retroactively rewrite their fiscal identifier, while customers and AEAT hold the original number. | | [`series_has_documents`](/errors/series_has_documents) | `invalid_request_error` | 422 | The series already numbered documents, so it cannot be removed: the correlative sequence has to stay auditable. | | [`series_immutable`](/errors/series_immutable) | `invalid_request_error` | 405 | Series are not editable nor deletable through the API: legal numbering continuity requires their prefix, year and counter to stay put. | | [`series_initial_number_creates_gap`](/errors/series_initial_number_creates_gap) | `invalid_request_error` | 422 | The starting number jumps beyond the next natural correlative while documents already exist for the current year, and that gap in the sequence is not acceptable to AEAT. | | [`series_locked_by_verifactu`](/errors/series_locked_by_verifactu) | `invalid_request_error` | 422 | At least one invoice of the series holds a billing record accepted by AEAT, which freezes the prefix, the year and the numbering base of the series. | | [`series_not_found`](/errors/series_not_found) | `not_found_error` | 404 | The identifier does not resolve to any numbering series of the authenticated company. | | [`series_type_invalid`](/errors/series_type_invalid) | `invalid_request_error` | 422 | The document type of the series is outside the catalogue `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`series_year_locked`](/errors/series_year_locked) | `invalid_request_error` | 422 | The series already issued documents in its current year. Moving the year would leave those documents pointing at an empty year while their taxable base sits in another. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Server error codes (/errors/index-server) Error codes emitted by Server. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | -------------------------------------------------------------- | --------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | [`dependency_unavailable`](/errors/dependency_unavailable) | `service_unavailable_error` | 503 | An external service the operation relies on did not answer in time. | | [`face_transmission_failed`](/errors/face_transmission_failed) | `api_error` | 502 | The FACe platform — the public administration entry point — was unreachable or answered with a fault. The failure is upstream, not in your request. | | [`facturae_signing_failed`](/errors/facturae_signing_failed) | `api_error` | 500 | The XAdES signature of the Facturae file could not be produced, usually because the signing certificate is unusable at that moment. | | [`internal_error`](/errors/internal_error) | `api_error` | 500 | Something broke on our side while processing the request. The condition is not caused by your payload. | | [`maintenance`](/errors/maintenance) | `service_unavailable_error` | 503 | The platform is in a maintenance window and writes are held back on purpose. | | [`pdf_generation_failed`](/errors/pdf_generation_failed) | `service_unavailable_error` | 503 | The rendering service could not produce the PDF. The document and its data are intact — what failed is the file. | | [`register_sealing_failed`](/errors/register_sealing_failed) | `api_error` | 500 | The cryptographic sealing of the record did not complete, so the closure was left unsigned rather than sealed with a broken signature. | | [`send_failed`](/errors/send_failed) | `api_error` | 500 | The document was not delivered by email: the mail provider rejected the message or was unreachable. | | [`service_unavailable`](/errors/service_unavailable) | `service_unavailable_error` | 503 | The service, or a dependency it needs, is temporarily unable to answer. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Suppliers error codes (/errors/index-suppliers) Error codes emitted by Suppliers. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [`supplier_has_documents`](/errors/supplier_has_documents) | `invalid_request_error` | 422 | The supplier is referenced by registered purchase invoices, and deleting it would leave those expenses without the party that issued them. | | [`supplier_not_found`](/errors/supplier_not_found) | `not_found_error` | 404 | The identifier does not resolve to any supplier of the authenticated company. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Tax Reports error codes (/errors/index-tax-reports) Error codes emitted by Tax Reports. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`insufficient_data_for_report`](/errors/insufficient_data_for_report) | `invalid_request_error` | 422 | The period has no data to file, or an invoice of the period lacks a mandatory field for this model — typically the customer tax id. | | [`invalid_period`](/errors/invalid_period) | `invalid_request_error` | 422 | The period does not identify a filing: the year is outside the accepted range, or the quarter is missing or out of the range 1 to 4 for a quarterly model. | | [`report_format_invalid`](/errors/report_format_invalid) | `invalid_request_error` | 422 | The format is outside the catalogue `txt_aeat`, `pdf`, `excel`. | | [`tax_report_not_found`](/errors/tax_report_not_found) | `not_found_error` | 404 | The identifier does not resolve to any tax report of the authenticated company. | | [`tax_report_type_invalid`](/errors/tax_report_type_invalid) | `invalid_request_error` | 422 | The report type is outside the catalogue `modelo_303`, `modelo_347`, `modelo_130`. | | [`unsupported_format`](/errors/unsupported_format) | `invalid_request_error` | 422 | The requested format is not available for this model: not every filing produces every output. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Taxes error codes (/errors/index-taxes) Error codes emitted by Taxes. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`custom_tax_creation_disabled`](/errors/custom_tax_creation_disabled) | `authorization_error` | 403 | Creating custom taxes is disabled for this company. | | [`duplicate_tax_default_for_document_type`](/errors/duplicate_tax_default_for_document_type) | `invalid_request_error` | 422 | Another tax of the same type is already the default for that document type, and the pair (tax type, document type) admits a single default. | | [`indirect_tax_regime_invalid`](/errors/indirect_tax_regime_invalid) | `invalid_request_error` | 422 | The indirect regime is outside the catalogue `iva`, `igic`, `ipsi`. | | [`invalid_aeat_code`](/errors/invalid_aeat_code) | `invalid_request_error` | 422 | The AEAT operation code is outside the closed catalogue `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` used by VeriFactu and SII. | | [`invalid_country_aeat_zone`](/errors/invalid_country_aeat_zone) | `invalid_request_error` | 422 | The AEAT territorial zone is outside the catalogue `peninsula`, `canarias`, `ceuta`, `melilla`. | | [`invalid_country_code`](/errors/invalid_country_code) | `invalid_request_error` | 422 | The country code is not exactly two characters, so it is not a valid ISO 3166-1 alpha-2 code. | | [`invalid_customer_visible_label`](/errors/invalid_customer_visible_label) | `invalid_request_error` | 422 | The label shown to the customer on the document exceeds the allowed length. | | [`invalid_description`](/errors/invalid_description) | `invalid_request_error` | 422 | The description exceeds the maximum length allowed for the field. | | [`invalid_document_type`](/errors/invalid_document_type) | `invalid_request_error` | 422 | The document type is outside the catalogue: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`invalid_rate_for_tax_regime`](/errors/invalid_rate_for_tax_regime) | `invalid_request_error` | 422 | The rate does not belong to the legal grid of its regime: IGIC admits 0, 3, 5, 7, 9.5, 15 and 20%; IPSI admits 0, 0.5, 1, 2, 4, 8 and 10%. | | [`invalid_tax_code`](/errors/invalid_tax_code) | `invalid_request_error` | 422 | The tax code is empty or longer than 50 characters. | | [`invalid_tax_name`](/errors/invalid_tax_name) | `invalid_request_error` | 422 | The tax name is empty or longer than 255 characters. | | [`invalid_tax_rate`](/errors/invalid_tax_rate) | `invalid_request_error` | 422 | The rate falls outside the range allowed for its type: VAT 0-27%, withholding 0-47%, equivalence surcharge 0-10%, other 0-100%. | | [`invalid_tax_type_filter`](/errors/invalid_tax_type_filter) | `invalid_request_error` | 422 | The `type` filter of the by-type listing carries a value outside the enum `vat`, `retention`, `surcharge`, `other`. | | [`invalid_validity_window`](/errors/invalid_validity_window) | `invalid_request_error` | 422 | The validity window is inverted: `valid_until` falls before `valid_from`. | | [`system_tax_default_modification_forbidden`](/errors/system_tax_default_modification_forbidden) | `authorization_error` | 403 | Defaults of the shared catalogue taxes are not set on the tax itself: the catalogue is global and the preference belongs to your company. | | [`system_tax_immutable`](/errors/system_tax_immutable) | `invalid_request_error` | 422 | The tax belongs to the canonical AEAT catalogue shipped with the product. Its rate, code and name are fixed so that every company shares the same fiscal reference. | | [`system_tax_immutable_field`](/errors/system_tax_immutable_field) | `invalid_request_error` | 422 | The update touches a field that is frozen on a system tax; `error.param` names it. | | [`system_tax_undeletable`](/errors/system_tax_undeletable) | `invalid_request_error` | 422 | System taxes are part of the shared fiscal catalogue and cannot be removed: deleting one would break the documents that reference it. | | [`tax_applies_to_invalid`](/errors/tax_applies_to_invalid) | `invalid_request_error` | 422 | The scope of the tax is outside the catalogue `sale`, `purchase`, `both`. | | [`tax_code_already_exists`](/errors/tax_code_already_exists) | `conflict_error` | 409 | Another tax of the catalogue already uses that code, and codes identify taxes unambiguously. | | [`tax_id_required`](/errors/tax_id_required) | `invalid_request_error` | 422 | The operation needs the tax identification number (NIF, CIF or NIE) of the party involved and the record does not carry one. | | [`tax_in_use`](/errors/tax_in_use) | `invalid_request_error` | 422 | The tax is referenced by documents, products or suppliers. Removing it would leave historical documents without their fiscal reference. | | [`tax_inactive_cannot_be_default`](/errors/tax_inactive_cannot_be_default) | `invalid_request_error` | 422 | A deactivated tax cannot become the default, either globally or for a document type — it would offer a hidden default that no form can pick. | | [`tax_not_found`](/errors/tax_not_found) | `not_found_error` | 404 | The identifier does not match any tax of the catalogue reachable by this company. | | [`tax_type_invalid`](/errors/tax_type_invalid) | `invalid_request_error` | 422 | The tax type is outside the catalogue `vat`, `retention`, `surcharge`, `other`. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # VeriFactu error codes (/errors/index-verifactu) Error codes emitted by VeriFactu. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alta_record_not_found`](/errors/alta_record_not_found) | `not_found_error` | 404 | The invoice has no registration record, so the operation that depends on it has nothing to work with. | | [`anulacion_record_already_exists`](/errors/anulacion_record_already_exists) | `conflict_error` | 409 | The invoice already carries an annulment record in the chain, and annulment is reported only once. | | [`certificate_expired`](/errors/certificate_expired) | `invalid_request_error` | 422 | The certificate is outside its validity window: it has expired, or it is not valid yet. | | [`certificate_nif_mismatch`](/errors/certificate_nif_mismatch) | `invalid_request_error` | 422 | The tax id of the certificate holder does not match the company tax id. AEAT records are signed on behalf of the company, so both must be the same. | | [`certificate_not_found`](/errors/certificate_not_found) | `not_found_error` | 404 | The company has no FNMT certificate matching the identifier, or none uploaded at all. | | [`certificate_too_large`](/errors/certificate_too_large) | `invalid_request_error` | 422 | The file exceeds the 100 KB limit, while a real FNMT certificate weighs a few kilobytes. | | [`clock_drift_exceeded`](/errors/clock_drift_exceeded) | `invalid_request_error` | 422 | The server clock drifted from NTP beyond the allowed margin. The generation timestamp is part of the AEAT fingerprint, so an unsynchronised clock would produce records AEAT rejects. | | [`declaracion_already_exists`](/errors/declaracion_already_exists) | `conflict_error` | 409 | The company already filed its SIF responsibility statement for that period. | | [`declaracion_not_found`](/errors/declaracion_not_found) | `not_found_error` | 404 | The company has no SIF responsibility statement filed for the requested period. | | [`event_already_processed`](/errors/event_already_processed) | `invalid_request_error` | 422 | That SIF event is already recorded in the event chain, and each event is processed exactly once. | | [`invalid_certificate_format`](/errors/invalid_certificate_format) | `invalid_request_error` | 422 | The file is not a PKCS#12 container: its first bytes do not match the ASN.1 structure the format requires, whatever its extension says. | | [`invalid_certificate_password`](/errors/invalid_certificate_password) | `invalid_request_error` | 422 | The password does not open the certificate file. | | [`max_retries_exceeded`](/errors/max_retries_exceeded) | `invalid_request_error` | 422 | The record exhausted the technical retry budget for resending the stored XML. Retrying the same content again would fail the same way. | | [`mode_switch_blocked_until_year_end`](/errors/mode_switch_blocked_until_year_end) | `invalid_request_error` | 422 | VeriFactu mode was activated during this fiscal year and at least one billing record was issued. Stepping back would degrade the integrity of a chain already reported to AEAT. | | [`record_already_accepted`](/errors/record_already_accepted) | `invalid_request_error` | 422 | AEAT already accepted the record. Acceptance is terminal and its content is frozen as part of the fingerprint chain. | | [`record_immutable`](/errors/record_immutable) | `invalid_request_error` | 422 | The record belongs to an append-only ledger: once written, its fiscal content is closed to changes and to deletion. | | [`record_not_rejected`](/errors/record_not_rejected) | `invalid_request_error` | 422 | The correction flow only applies to records AEAT rejected on data grounds. This record is in another state — a technical failure, for instance, is covered by the automatic retry. | | [`record_not_subsanable`](/errors/record_not_subsanable) | `invalid_request_error` | 422 | The record cannot be amended: it is not a registration record, or it has no source invoice from which its content could be regenerated. | | [`requires_annulment`](/errors/requires_annulment) | `invalid_request_error` | 422 | The regenerated content changes a field that takes part in the fingerprint — issuer tax id, series and number, issue date, invoice type, tax amount or total — and the chain cannot be rewritten. | | [`sii_excluded`](/errors/sii_excluded) | `invalid_request_error` | 422 | The company is registered with SII, and SII filers are excluded from the VeriFactu regulation. | | [`verifactu_already_submitted`](/errors/verifactu_already_submitted) | `invalid_request_error` | 422 | The invoice already has its registration record. Exactly one registration exists per invoice, so a second one would break the idempotency of the chain. | | [`verifactu_mode_invalid`](/errors/verifactu_mode_invalid) | `invalid_request_error` | 422 | The mode is outside the catalogue `verifactu` / `no_verifactu`. | | [`verifactu_not_eligible`](/errors/verifactu_not_eligible) | `invalid_request_error` | 422 | The invoice cannot be registered with AEAT right now: the company is not on VeriFactu mode, it has no active certificate, or the certificate is revoked or issued for a different tax id. | | [`verifactu_record_not_found`](/errors/verifactu_record_not_found) | `not_found_error` | 404 | The identifier does not match any billing record of the authenticated company. | | [`verifactu_transmission_failed`](/errors/verifactu_transmission_failed) | `invalid_request_error` | 422 | The transmission of the record to AEAT did not complete: the endpoint was unreachable or answered with an incident. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # Webhooks error codes (/errors/index-webhooks) Error codes emitted by Webhooks. Each `code` links to its own page with the cause and the action to take. | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_required`](/errors/addon_required) | `payment_required_error` | 402 | Creating webhook endpoints belongs to the Developer API add-on, and the company does not have it active — the free tier allows zero endpoints. | | [`api_version_invalid_format`](/errors/api_version_invalid_format) | `invalid_request_error` | 422 | The payload version of the endpoint is not a `YYYY-MM-DD` date. | | [`api_version_unsupported`](/errors/api_version_unsupported) | `invalid_request_error` | 422 | The payload version is well formed but is not among the ones the platform serves. | | [`custom_header_blocklisted`](/errors/custom_header_blocklisted) | `invalid_request_error` | 422 | One of the custom headers is reserved: the HTTP layer manages it (`host`, `content-type`, `content-length`, `user-agent`), Factuarea sends it as part of the signed contract (`factuarea-*`), or the proxy owns it (`x-forwarded-*`). | | [`custom_header_value_too_long`](/errors/custom_header_value_too_long) | `invalid_request_error` | 422 | The value of a custom header exceeds 1024 characters. | | [`replay_delivery_not_retryable`](/errors/replay_delivery_not_retryable) | `invalid_request_error` | 422 | Only failed deliveries can be replayed. A delivery that succeeded, or one still in flight, has nothing to resend. | | [`replay_event_expired`](/errors/replay_event_expired) | `invalid_request_error` | 422 | The event behind the delivery was purged by the 30-day retention policy, so there is no payload left to resend. | | [`timeout_seconds_out_of_range`](/errors/timeout_seconds_out_of_range) | `invalid_request_error` | 422 | `timeout_seconds` falls outside the range 1 to 30 seconds. | | [`too_many_custom_headers`](/errors/too_many_custom_headers) | `invalid_request_error` | 422 | The endpoint declares more than 20 custom headers. | | [`webhook_delivery_not_found`](/errors/webhook_delivery_not_found) | `not_found_error` | 404 | The identifier does not match any delivery attempt, or the delivery falls outside the retention window kept for the history. | | [`webhook_endpoint_degraded`](/errors/webhook_endpoint_degraded) | `invalid_request_error` | 422 | The endpoint is degraded after repeated delivery failures, so test pings are refused while it stays in that state. | | [`webhook_endpoint_not_found`](/errors/webhook_endpoint_not_found) | `not_found_error` | 404 | The identifier does not resolve to any webhook endpoint of the authenticated company. | | [`webhook_secret_recently_rotated`](/errors/webhook_secret_recently_rotated) | `rate_limit_error` | 429 | The signing secret was rotated less than five minutes ago. The grace window lets your receiver accept both secrets during the switch; rotating again inside it would invalidate signatures still in flight. | ## Related [#related] * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # indirect_tax_regime_invalid (/errors/indirect_tax_regime_invalid) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------- | | `indirect_tax_regime_invalid` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The indirect regime is outside the catalogue `iva`, `igic`, `ipsi`. ## What to do [#what-to-do] Send one of the three regimes, or let it be derived from the AEAT zone instead of stating it by hand. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # insufficient_data_for_report (/errors/insufficient_data_for_report) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ---------------------------------------- | | `insufficient_data_for_report` | `invalid_request_error` | 422 | [Tax Reports](/errors/index-tax-reports) | ## Cause [#cause] The period has no data to file, or an invoice of the period lacks a mandatory field for this model — typically the customer tax id. ## What to do [#what-to-do] Read `error.subcode`: complete the missing data on the invoices it points at, or pick a period with activity. ## Related [#related] * [All Tax Reports error codes](/errors/index-tax-reports) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # insufficient_scope (/errors/insufficient_scope) | Code | Type | HTTP | Category | | -------------------- | --------------------- | ---- | -------------------------------------------- | | `insufficient_scope` | `authorization_error` | 403 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The key authenticates correctly but does not carry the scope this operation requires. Scopes are granted when the key is issued and are not widened at call time. ## What to do [#what-to-do] Issue a key including the scope named in `error.message` — read scopes for queries, write scopes for changes — and use it for this call. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # internal_error (/errors/internal_error) | Code | Type | HTTP | Category | | ---------------- | ----------- | ---- | ------------------------------ | | `internal_error` | `api_error` | 500 | [Server](/errors/index-server) | ## Cause [#cause] Something broke on our side while processing the request. The condition is not caused by your payload. ## What to do [#what-to-do] Retry with exponential back-off, reusing the same `Idempotency-Key` on writes, and report the `request_id` if it persists. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_aeat_code (/errors/invalid_aeat_code) | Code | Type | HTTP | Category | | ------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_aeat_code` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The AEAT operation code is outside the closed catalogue `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` used by VeriFactu and SII. ## What to do [#what-to-do] Pick the code matching the fiscal nature of the operation: `S1` subject and not exempt, `S2` reverse charge, `E1`-`E6` exemptions, `N1`-`N2` out of scope. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_api_key (/errors/invalid_api_key) | Code | Type | HTTP | Category | | ----------------- | ---------------------- | ---- | ---------------------------------------------- | | `invalid_api_key` | `authentication_error` | 401 | [Authentication](/errors/index-authentication) | ## Cause [#cause] The key does not match any active key. It may be mistyped, truncated, or belong to a different environment — test keys and live keys are not interchangeable. ## What to do [#what-to-do] Copy the key again from the dashboard and check the environment: `fact_test_` keys only work in test mode, `fact_live_` keys only in production. ## Message returned by the API [#message-returned-by-the-api] > Invalid API key. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_certificate_format (/errors/invalid_certificate_format) | Code | Type | HTTP | Category | | ---------------------------- | ----------------------- | ---- | ------------------------------------ | | `invalid_certificate_format` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The file is not a PKCS#12 container: its first bytes do not match the ASN.1 structure the format requires, whatever its extension says. ## What to do [#what-to-do] Upload the original `.p12` or `.pfx` file; a PEM, a CRT or a renamed file will not be accepted. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_certificate_password (/errors/invalid_certificate_password) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ------------------------------------ | | `invalid_certificate_password` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The password does not open the certificate file. ## What to do [#what-to-do] Send the password protecting the `.p12` exactly as it was set — spaces and capitalisation count. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_correction_nature (/errors/invalid_correction_nature) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_correction_nature` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] `correction_nature` only accepts `S` (substitution: the corrective carries the full corrected amounts) or `I` (by difference: it carries only the delta). ## What to do [#what-to-do] Send `S` when the corrective replaces the original amounts, and `I` when it only states the difference. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_correction_reason (/errors/invalid_correction_reason) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_correction_reason` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The correction reason is outside the closed fiscal list (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), which maps to the AEAT codes R1 to R4. ## What to do [#what-to-do] Pick the reason that matches the real cause: it decides the code reported to AEAT, and `concurso` and `incobrable` require supporting documentation. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_country_aeat_zone (/errors/invalid_country_aeat_zone) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_country_aeat_zone` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The AEAT territorial zone is outside the catalogue `peninsula`, `canarias`, `ceuta`, `melilla`. ## What to do [#what-to-do] Send the zone matching the territory of the tax: it decides the indirect regime (VAT, IGIC or IPSI) and the legal grid of rates. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_country_code (/errors/invalid_country_code) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_country_code` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The country code is not exactly two characters, so it is not a valid ISO 3166-1 alpha-2 code. ## What to do [#what-to-do] Send the two-letter code of the country (`ES`, `FR`, `PT`). ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_customer_visible_label (/errors/invalid_customer_visible_label) | Code | Type | HTTP | Category | | -------------------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_customer_visible_label` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The label shown to the customer on the document exceeds the allowed length. ## What to do [#what-to-do] Shorten the label; it is meant as a short caption on the document line, not as a description. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_description (/errors/invalid_description) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_description` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The description exceeds the maximum length allowed for the field. ## What to do [#what-to-do] Shorten the description; the identifying detail belongs in the name and the code, not here. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_document_type (/errors/invalid_document_type) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_document_type` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The document type is outside the catalogue: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. ## What to do [#what-to-do] Send one of those values in the field that selects the document type. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_expiry_date (/errors/invalid_expiry_date) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------------ | | `invalid_expiry_date` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The expiry date is earlier than the issue date, or more than 365 days after it. ## What to do [#what-to-do] Send an expiry date between the issue date and 365 days later. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_frequency_interval (/errors/invalid_frequency_interval) | Code | Type | HTTP | Category | | ---------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `invalid_frequency_interval` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The interval is lower than 1, so the recurrence would never advance to a next run. ## What to do [#what-to-do] Send an interval of 1 or more: it multiplies the frequency, as in `monthly` with interval 2 for every two months. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_frequency_type (/errors/invalid_frequency_type) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ------------------------------------------------------ | | `invalid_frequency_type` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The frequency is outside the catalogue `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. ## What to do [#what-to-do] Pick one of the frequencies; use `custom` with an explicit interval when none of the named ones fits. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_holiday_handling (/errors/invalid_holiday_handling) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `invalid_holiday_handling` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The holiday policy is outside the catalogue `skip`, `before`, `after`, `same`. ## What to do [#what-to-do] Choose what should happen when a run falls on a holiday: skip it, move it earlier, move it later, or issue on the same date anyway. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_invoice_id (/errors/invalid_invoice_id) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_invoice_id` | `invalid_request_error` | 400 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice reference received is not a valid identifier; it usually means an internal value slipped in where the API expects the public `id`. ## What to do [#what-to-do] Send the invoice `id` returned by the API; internal numeric identifiers are not part of the v1 contract. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_invoice_number (/errors/invalid_invoice_number) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ---------------------------------- | | `invalid_invoice_number` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice number does not follow the canonical format `SERIES-YYYY-NNN`, plus the `-RECn` suffix on correctives. ## What to do [#what-to-do] Send the number exactly as it appears on the invoice instead of assembling it from separate parts. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_invoice_status (/errors/invalid_invoice_status) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ---------------------------------- | | `invalid_invoice_status` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The value sent as invoice status is outside the lifecycle catalogue (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). ## What to do [#what-to-do] Use one of the catalogue values, spelled exactly as the API returns them. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_invoice_uuid (/errors/invalid_invoice_uuid) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_invoice_uuid` | `invalid_request_error` | 400 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice identifier in the path or in the payload is not a valid UUID. ## What to do [#what-to-do] Copy the `id` exactly as the API returned it, with no truncation or re-encoding. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_param_format (/errors/invalid_param_format) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | -------------------------------- | | `invalid_param_format` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] A legacy form request rejected the shape of a value. Migrated endpoints report the same situation as `parameter_invalid_format` or `parameter_invalid_integer`. ## What to do [#what-to-do] Fix the format of the field in `error.param`; when branching on error codes, treat this one as an alias of `parameter_invalid_format`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_param_value (/errors/invalid_param_value) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | -------------------------------- | | `invalid_param_value` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] A legacy form request rejected the value of a field. Migrated endpoints report the same situation as `parameter_invalid_enum` or `parameter_invalid_range`. ## What to do [#what-to-do] Correct the value of `error.param`; when branching on error codes, treat this one as an alias of `parameter_invalid_enum`. ## Message returned by the API [#message-returned-by-the-api] > The value of one or more parameters is invalid. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_payment_date (/errors/invalid_payment_date) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_payment_date` | `invalid_request_error` | 422 | [Payments](/errors/index-payments) | ## Cause [#cause] The payment date falls outside the accepted window: it cannot precede the invoice issue date, nor be in the future. ## What to do [#what-to-do] Send a date between the issue date and today, both included. ## Related [#related] * [All Payments error codes](/errors/index-payments) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_payment_method (/errors/invalid_payment_method) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ---------------------------------- | | `invalid_payment_method` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The payment method is outside the closed allowlist: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. ## What to do [#what-to-do] Send one of those seven values; the list is closed and cannot be extended per company. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_period (/errors/invalid_period) | Code | Type | HTTP | Category | | ---------------- | ----------------------- | ---- | ---------------------------------------- | | `invalid_period` | `invalid_request_error` | 422 | [Tax Reports](/errors/index-tax-reports) | ## Cause [#cause] The period does not identify a filing: the year is outside the accepted range, or the quarter is missing or out of the range 1 to 4 for a quarterly model. ## What to do [#what-to-do] Send a valid year and, for Modelo 303 and Modelo 130, the quarter of the filing; Modelo 347 is annual and takes no quarter. ## Related [#related] * [All Tax Reports error codes](/errors/index-tax-reports) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_proforma_id (/errors/invalid_proforma_id) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------------ | | `invalid_proforma_id` | `invalid_request_error` | 400 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The pro forma reference received is not a valid identifier, usually because an internal value replaced the public `id`. ## What to do [#what-to-do] Send the `id` the API returns for the pro forma. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_proforma_number (/errors/invalid_proforma_number) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ------------------------------------ | | `invalid_proforma_number` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The pro forma number does not follow the canonical numbering format of its series. ## What to do [#what-to-do] Send the number exactly as it appears on the document, series prefix and year included. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_proforma_status (/errors/invalid_proforma_status) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ------------------------------------ | | `invalid_proforma_status` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The value sent as status is outside the catalogue `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. ## What to do [#what-to-do] Use one of the catalogue values, spelled as the API returns them. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_proforma_uuid (/errors/invalid_proforma_uuid) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ------------------------------------ | | `invalid_proforma_uuid` | `invalid_request_error` | 400 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The pro forma identifier in the path or in the payload is not a valid UUID. ## What to do [#what-to-do] Copy the `id` exactly as the API returned it. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_purchase_invoice_id (/errors/invalid_purchase_invoice_id) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `invalid_purchase_invoice_id` | `invalid_request_error` | 400 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The purchase invoice reference received is not a valid identifier, usually because an internal value replaced the public `id`. ## What to do [#what-to-do] Send the `id` the API returns for the purchase invoice. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_purchase_invoice_number (/errors/invalid_purchase_invoice_number) | Code | Type | HTTP | Category | | --------------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `invalid_purchase_invoice_number` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The invoice number is empty or does not fit the accepted format. On a purchase invoice the number is the one the supplier printed, not one Factuarea generates. ## What to do [#what-to-do] Copy the number from the supplier document exactly as it appears there. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_purchase_invoice_uuid (/errors/invalid_purchase_invoice_uuid) | Code | Type | HTTP | Category | | ------------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `invalid_purchase_invoice_uuid` | `invalid_request_error` | 400 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The purchase invoice identifier in the path or in the payload is not a valid UUID. ## What to do [#what-to-do] Copy the `id` exactly as the API returned it. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_rate_for_tax_regime (/errors/invalid_rate_for_tax_regime) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_rate_for_tax_regime` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The rate does not belong to the legal grid of its regime: IGIC admits 0, 3, 5, 7, 9.5, 15 and 20%; IPSI admits 0, 0.5, 1, 2, 4, 8 and 10%. ## What to do [#what-to-do] Pick a rate from the grid of the regime; if you meant a VAT rate, check the AEAT zone of the tax is `peninsula`. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_recurring_invoice_id (/errors/invalid_recurring_invoice_id) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ------------------------------------------------------ | | `invalid_recurring_invoice_id` | `invalid_request_error` | 400 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence reference received is not a valid identifier, usually because an internal value replaced the public `id`. ## What to do [#what-to-do] Send the `id` the API returns for the recurrence. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_recurring_invoice_uuid (/errors/invalid_recurring_invoice_uuid) | Code | Type | HTTP | Category | | -------------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `invalid_recurring_invoice_uuid` | `invalid_request_error` | 400 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence identifier in the path or in the payload is not a valid UUID. ## What to do [#what-to-do] Copy the `id` exactly as the API returned it. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_series_code (/errors/invalid_series_code) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------ | | `invalid_series_code` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The series code is empty, too long, or carries characters that do not belong in a fiscal prefix. ## What to do [#what-to-do] Send a short alphanumeric prefix; it is stored upper-cased and becomes part of every document number of the series. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_series_name (/errors/invalid_series_name) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------ | | `invalid_series_name` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The series name is empty or exceeds the allowed length. ## What to do [#what-to-do] Send a short descriptive name; the fiscal identifier is the code, not the name. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_series_number (/errors/invalid_series_number) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ------------------------------ | | `invalid_series_number` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The starting number is not valid: it is not a positive integer, or it falls at or below the last number already issued, which would re-issue numbers already in use. ## What to do [#what-to-do] Send a starting number above the current counter, or leave it out to continue the natural sequence. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_series_uuid (/errors/invalid_series_uuid) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------ | | `invalid_series_uuid` | `invalid_request_error` | 400 | [Series](/errors/index-series) | ## Cause [#cause] The series identifier in the path or in the payload is not a valid UUID. ## What to do [#what-to-do] Copy the `id` exactly as the API returned it. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_series_year (/errors/invalid_series_year) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------ | | `invalid_series_year` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The fiscal year is not a valid four-digit year for a numbering series. ## What to do [#what-to-do] Send the year as a four-digit number matching the fiscal year the series numbers. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_status_transition (/errors/invalid_status_transition) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | -------------------------------- | | `invalid_status_transition` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] The requested state is not reachable from the state the document is in right now. ## What to do [#what-to-do] Read the current `status` and walk the intermediate steps the document lifecycle requires before asking for the target state. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_tax_code (/errors/invalid_tax_code) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ---------------------------- | | `invalid_tax_code` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The tax code is empty or longer than 50 characters. ## What to do [#what-to-do] Send a non-empty code of up to 50 characters that identifies the tax within your catalogue. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_tax_name (/errors/invalid_tax_name) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ---------------------------- | | `invalid_tax_name` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The tax name is empty or longer than 255 characters. ## What to do [#what-to-do] Send a non-empty name of up to 255 characters. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_tax_rate (/errors/invalid_tax_rate) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ---------------------------- | | `invalid_tax_rate` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The rate falls outside the range allowed for its type: VAT 0-27%, withholding 0-47%, equivalence surcharge 0-10%, other 0-100%. ## What to do [#what-to-do] Send a rate inside the range of the tax type, as a percentage rather than a fraction (`21`, not `0.21`). ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_tax_type_filter (/errors/invalid_tax_type_filter) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_tax_type_filter` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The `type` filter of the by-type listing carries a value outside the enum `vat`, `retention`, `surcharge`, `other`. ## What to do [#what-to-do] Send one of the four types, or drop the filter to list the whole catalogue. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invalid_validity_window (/errors/invalid_validity_window) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------- | | `invalid_validity_window` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The validity window is inverted: `valid_until` falls before `valid_from`. ## What to do [#what-to-do] Send `valid_until` equal to or later than `valid_from`, or omit it if the tax has no end date. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_already_annulled (/errors/invoice_already_annulled) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_already_annulled` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice was already annulled. Annulment is terminal and, with VeriFactu active, its annulment record has already reached AEAT. ## What to do [#what-to-do] Do not repeat the annulment; if the operation must be billed again, issue a new invoice. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_already_paid (/errors/invoice_already_paid) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_already_paid` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice is already settled. `paid` is a terminal, accounting-closed state: the output VAT has been declared, or will be declared for the period. ## What to do [#what-to-do] Correct a paid invoice by issuing a corrective invoice that references it; it can no longer be edited or voided. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_already_sent (/errors/invoice_already_sent) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_already_sent` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice was already issued: it holds a definitive series number and, with VeriFactu active, its registration with AEAT. Issuing does not happen twice. ## What to do [#what-to-do] Skip the issuing step; to deliver it again use the send operation, and to change its content issue a corrective invoice. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_cannot_assign_number (/errors/invoice_cannot_assign_number) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ---------------------------------- | | `invoice_cannot_assign_number` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] A definitive number was requested for an invoice that is not a draft, or that already carries one. Series numbering is monotonic and numbers are never reassigned. ## What to do [#what-to-do] Only ask for a number on a draft that still shows the placeholder; if the invoice already has one, read it from the `number` field. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_invalid_status_transition (/errors/invoice_invalid_status_transition) | Code | Type | HTTP | Category | | ----------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_invalid_status_transition` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The target status is unreachable from the current one. The lifecycle is directed: `draft` moves to `scheduled` or `sent`, `sent` to `paid`, `overdue` or `annulled`, and `paid`, `cancelled` and `annulled` are terminal. ## What to do [#what-to-do] Read the current `status` and call the operation for the step you actually need, instead of setting the target status directly. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_cancellable_in_current_state (/errors/invoice_not_cancellable_in_current_state) | Code | Type | HTTP | Category | | ------------------------------------------ | ----------------------- | ---- | ---------------------------------- | | `invoice_not_cancellable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Cancelling withdraws a draft that is not yet fiscally binding, so it only applies while the invoice is `draft`. ## What to do [#what-to-do] If the invoice is already issued, annul it instead; if it is paid, correct it with a corrective invoice. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_correctable_in_current_state (/errors/invoice_not_correctable_in_current_state) | Code | Type | HTTP | Category | | ------------------------------------------ | ----------------------- | ---- | ---------------------------------- | | `invoice_not_correctable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] A corrective invoice can only be issued against an invoice that is already issued (`sent` or `paid`). A draft, a cancelled or an annulled invoice has nothing to correct. ## What to do [#what-to-do] Issue the original invoice first; while it is still a draft, edit it directly instead of correcting it. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_deletable_in_current_state (/errors/invoice_not_deletable_in_current_state) | Code | Type | HTTP | Category | | ---------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Only `draft` and `cancelled` invoices can be deleted. A numbered invoice never disappears: the correlative sequence must stay auditable. ## What to do [#what-to-do] Cancel the draft, or annul the issued invoice; deletion is not a path for it. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_editable_in_current_state (/errors/invoice_not_editable_in_current_state) | Code | Type | HTTP | Category | | --------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_editable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Only a draft admits editing. Once issued, the invoice is immutable and its content is frozen along with its fiscal record. ## What to do [#what-to-do] Issue a corrective invoice with the right amounts instead of editing this one. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_eligible_for_action (/errors/invoice_not_eligible_for_action) | Code | Type | HTTP | Category | | --------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_eligible_for_action` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The requested action does not apply to this invoice: its type or its current state leaves it outside the scope of the operation. ## What to do [#what-to-do] Read `status` and `type` on the invoice and call the operation that matches them; the reference states which states each action accepts. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_found (/errors/invoice_not_found) | Code | Type | HTTP | Category | | ------------------- | ----------------- | ---- | ---------------------------------- | | `invoice_not_found` | `not_found_error` | 404 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The identifier does not resolve to any invoice of the authenticated company. Invoices belonging to another company answer exactly the same way. ## What to do [#what-to-do] Check the `id` and the active profile; if you only hold your own reference, look the invoice up by `external_id` or by invoice number. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_modifiable_in_current_state (/errors/invoice_not_modifiable_in_current_state) | Code | Type | HTTP | Category | | ----------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_modifiable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The field you are changing is frozen for the current state — for instance the tax regime of an annulled invoice. ## What to do [#what-to-do] Read `error.message` to see which field is involved; on issued invoices, changes travel through a corrective invoice. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_paid (/errors/invoice_not_paid) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ---------------------------------- | | `invoice_not_paid` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] A payment receipt was requested for an invoice with no settled payment, so there is nothing to certify. ## What to do [#what-to-do] Register the payment first, then request the receipt. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_reschedulable_in_current_state (/errors/invoice_not_reschedulable_in_current_state) | Code | Type | HTTP | Category | | -------------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_reschedulable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Rescheduling moves the issuing date of an invoice that is waiting in `scheduled`, and this invoice is not waiting. ## What to do [#what-to-do] Check the `status`: if it is `draft`, schedule it; if it is already `sent`, the issuing happened and the date cannot move. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_schedulable_in_current_state (/errors/invoice_not_schedulable_in_current_state) | Code | Type | HTTP | Category | | ------------------------------------------ | ----------------------- | ---- | ---------------------------------- | | `invoice_not_schedulable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Only a draft can be scheduled: scheduling reserves a future issuing moment without consuming a series number yet. ## What to do [#what-to-do] Schedule the invoice while it is still a draft; if it is already issued there is nothing left to schedule. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_unschedulable_in_current_state (/errors/invoice_not_unschedulable_in_current_state) | Code | Type | HTTP | Category | | -------------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_unschedulable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Unscheduling returns an invoice from `scheduled` to `draft`, so it only applies while it is still waiting to be issued. ## What to do [#what-to-do] If the scheduled issuing already ran, the invoice is `sent`: undo it by annulling it or by issuing a corrective invoice. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_not_unsendable_in_current_state (/errors/invoice_not_unsendable_in_current_state) | Code | Type | HTTP | Category | | ----------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_not_unsendable_in_current_state` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Undoing the delivery mark only applies to a `sent` invoice: it clears `sent_at` and keeps the invoice issued. ## What to do [#what-to-do] Do not use it on paid, overdue, annulled or cancelled invoices — those need a corrective invoice or an annulment, not an undo. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_requires_at_least_one_line (/errors/invoice_requires_at_least_one_line) | Code | Type | HTTP | Category | | ------------------------------------ | ----------------------- | ---- | ---------------------------------- | | `invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice carries no operation line, so it has no taxable base and cannot be issued. This happens both when you send no lines at all and when every line you send is a disbursement: a disbursement is an amount paid on the customer's behalf (art. 78.Tres.3 LIVA), not an operation of your own. ## What to do [#what-to-do] Add at least one operation line (`line_type` NORMAL, the default) with description, quantity and unit price. If you actually mean to bill the expense as your own, charge it on a normal line with its VAT rate instead of declaring it a disbursement. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # invoice_year_required_for_ambiguous_number (/errors/invoice_year_required_for_ambiguous_number) | Code | Type | HTTP | Category | | -------------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `invoice_year_required_for_ambiguous_number` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] That invoice number exists in more than one fiscal year, so on its own it does not identify a single invoice. ## What to do [#what-to-do] Repeat the lookup adding `year`; `error.message` lists the years where the number exists. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # ip_not_allowed (/errors/ip_not_allowed) | Code | Type | HTTP | Category | | ---------------- | ---------------------- | ---- | ---------------------------------------------- | | `ip_not_allowed` | `authentication_error` | 401 | [Authentication](/errors/index-authentication) | ## Cause [#cause] The key restricts the addresses it accepts, and the request came from one outside that list. ## What to do [#what-to-do] Add the outbound address of your server to the key allowlist, or use a key without an IP restriction for clients whose address changes. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # length_required (/errors/length_required) | Code | Type | HTTP | Category | | ----------------- | ----------------------- | ---- | -------------------------------- | | `length_required` | `invalid_request_error` | 411 | [Request](/errors/index-request) | ## Cause [#cause] A request with a body arrived using chunked transfer encoding, without declaring its size. The API needs the length up front to reject oversized payloads before buffering them. ## What to do [#what-to-do] Send the body with a `Content-Length` header instead of `Transfer-Encoding: chunked`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # line_total_checksum_mismatch (/errors/line_total_checksum_mismatch) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ---------------------------------- | | `line_total_checksum_mismatch` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The `line_total` you declared does not match the one Factuarea computes for that line (quantity × price − discount + VAT − withholding + surcharge) and the deviation is above the one-cent tolerance. The amount that gets invoiced and reported to the tax authority is always the one computed here, so the discrepancy means your system and the issued invoice would not reconcile. ## What to do [#what-to-do] Compare `error.details.expected` (our total) with `error.details.received` (yours) and fix the rounding on your side. The field is an optional input checksum that is never persisted, so you can also omit it and take the amounts from the response. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # line_type_invalid (/errors/line_type_invalid) | Code | Type | HTTP | Category | | ------------------- | ----------------------- | ---- | ---------------------------------- | | `line_type_invalid` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The line type falls outside the closed `NORMAL` / `SUPLIDO` catalogue. An issued invoice only tells two natures apart: what you sell, which forms the taxable base and carries VAT, and a disbursement (`suplido`), money advanced in the name and on behalf of the customer, which is therefore left out of the base (art. 78.Tres.3 of the Spanish VAT Act). ## What to do [#what-to-do] Send `NORMAL` for whatever you invoice as your own and `SUPLIDO` only for amounts you pay to a third party on the customer's behalf; `error.details.allowed_values` carries the exact catalogue. An expense of yours that you pass on is not a disbursement: it goes as `NORMAL` with its VAT rate. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # maintenance (/errors/maintenance) | Code | Type | HTTP | Category | | ------------- | --------------------------- | ---- | ------------------------------ | | `maintenance` | `service_unavailable_error` | 503 | [Server](/errors/index-server) | ## Cause [#cause] The platform is in a maintenance window and writes are held back on purpose. ## What to do [#what-to-do] Retry once the window closes; queue the writes on your side so nothing is lost meanwhile. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # max_api_keys_exceeded (/errors/max_api_keys_exceeded) | Code | Type | HTTP | Category | | ----------------------- | --------------------- | ---- | -------------------------------------------- | | `max_api_keys_exceeded` | `authorization_error` | 422 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The company reached the number of API keys its plan allows. ## What to do [#what-to-do] Revoke keys you no longer use before issuing a new one, or upgrade the plan if you really need more of them at once. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # max_retries_exceeded (/errors/max_retries_exceeded) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ------------------------------------ | | `max_retries_exceeded` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The record exhausted the technical retry budget for resending the stored XML. Retrying the same content again would fail the same way. ## What to do [#what-to-do] Read the AEAT error, fix the underlying data and use the correction flow: it regenerates the content and resets the retry round. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # max_webhook_endpoints_exceeded (/errors/max_webhook_endpoints_exceeded) | Code | Type | HTTP | Category | | -------------------------------- | --------------------- | ---- | -------------------------------------------- | | `max_webhook_endpoints_exceeded` | `authorization_error` | 422 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The company reached the number of webhook endpoints its add-on tier allows. ## What to do [#what-to-do] Delete endpoints you no longer listen to, or move to a tier with a higher limit; one endpoint can subscribe to several event types. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # metadata_too_many_keys (/errors/metadata_too_many_keys) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `metadata_too_many_keys` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] The `metadata` object exceeds the limit of 50 keys per resource. ## What to do [#what-to-do] Trim `metadata` to 50 keys or fewer and keep the rest on your side, indexed by the resource `id`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # metadata_value_too_long (/errors/metadata_value_too_long) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `metadata_value_too_long` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] One value of `metadata` exceeds 500 characters once serialised to text. ## What to do [#what-to-do] Shorten that value below 500 characters, or store the long content in your system and keep only a reference in `metadata`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # method_not_allowed (/errors/method_not_allowed) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | -------------------------------- | | `method_not_allowed` | `invalid_request_error` | 405 | [Request](/errors/index-request) | ## Cause [#cause] The path exists but does not accept the HTTP verb used. ## What to do [#what-to-do] Check the verb in the endpoint reference; the `Allow` header of the response lists the ones this path accepts. ## Message returned by the API [#message-returned-by-the-api] > HTTP method not allowed for this route. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # missing_api_key (/errors/missing_api_key) | Code | Type | HTTP | Category | | ----------------- | ---------------------- | ---- | ---------------------------------------------- | | `missing_api_key` | `authentication_error` | 401 | [Authentication](/errors/index-authentication) | ## Cause [#cause] The request carries no credentials: neither the `Authorization` header nor `X-API-Key`. ## What to do [#what-to-do] Send `Authorization: Bearer <your key>`; the key travels in the header, never in the query string. ## Message returned by the API [#message-returned-by-the-api] > Missing Authorization or X-API-Key header. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # missing_required_param (/errors/missing_required_param) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `missing_required_param` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] A legacy form request found a required field missing. Endpoints already migrated to the canonical parsers report the same situation as `parameter_missing`. ## What to do [#what-to-do] Add the missing field; when branching on error codes, treat this one as an alias of `parameter_missing`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # mode_switch_blocked_until_year_end (/errors/mode_switch_blocked_until_year_end) | Code | Type | HTTP | Category | | ------------------------------------ | ----------------------- | ---- | ------------------------------------ | | `mode_switch_blocked_until_year_end` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] VeriFactu mode was activated during this fiscal year and at least one billing record was issued. Stepping back would degrade the integrity of a chain already reported to AEAT. ## What to do [#what-to-do] Wait until 31 December of the current year; the downgrade is only available while the company has not issued its first record. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # module_not_available_in_sandbox (/errors/module_not_available_in_sandbox) | Code | Type | HTTP | Category | | --------------------------------- | --------------------- | ---- | -------------------------------------------- | | `module_not_available_in_sandbox` | `authorization_error` | 403 | [Authorization](/errors/index-authorization) | ## Cause [#cause] The resource belongs to a module vetoed in test mode. Sandbox never touches AEAT, banks or real billing, so those modules stay out on purpose. ## What to do [#what-to-do] Try the operation with a live key on a real company; this restriction is about the environment, not about the plan. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # monthly_quota_exceeded (/errors/monthly_quota_exceeded) | Code | Type | HTTP | Category | | ------------------------ | ------------------ | ---- | -------------------------------------- | | `monthly_quota_exceeded` | `rate_limit_error` | 429 | [Rate Limit](/errors/index-rate-limit) | ## Cause [#cause] The company exhausted the monthly call quota its plan includes. ## What to do [#what-to-do] Wait for the next billing cycle or upgrade the plan; meanwhile, cut polling by subscribing to webhooks instead of re-reading collections. ## Related [#related] * [All Rate Limit error codes](/errors/index-rate-limit) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # monthly_requires_month_segmented_format (/errors/monthly_requires_month_segmented_format) | Code | Type | HTTP | Category | | ----------------------------------------- | ----------------------- | ---- | ------------------------------ | | `monthly_requires_month_segmented_format` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The counter resets monthly but the numbering mask does not segment by month, so two months would start on the same correlative and produce duplicate numbers within the year. ## What to do [#what-to-do] Add the month token to `number_format`, or switch the reset policy to annual or never. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # no_invoices_in_period (/errors/no_invoices_in_period) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ---------------------------------- | | `no_invoices_in_period` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The quarterly operation found no invoices in the requested period, so there is nothing to package or send. ## What to do [#what-to-do] Check the year and the quarter and pick a period with issued invoices. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # notification_not_found (/errors/notification_not_found) | Code | Type | HTTP | Category | | ------------------------ | ----------------- | ---- | -------------------------------------------- | | `notification_not_found` | `not_found_error` | 404 | [Notifications](/errors/index-notifications) | ## Cause [#cause] The identifier does not match any notification of the authenticated company, or the notification fell out of the retention window. ## What to do [#what-to-do] List the notifications to get a current `id`. ## Related [#related] * [All Notifications error codes](/errors/index-notifications) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # operation_regime_invalid (/errors/operation_regime_invalid) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `operation_regime_invalid` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The operation regime is outside the catalogue `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. ## What to do [#what-to-do] Pick the regime matching the operation: it decides how VAT is reported and whether the reverse charge applies. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # origin_not_allowed (/errors/origin_not_allowed) | Code | Type | HTTP | Category | | -------------------- | ---------------------- | ---- | ---------------------------------------------- | | `origin_not_allowed` | `authentication_error` | 401 | [Authentication](/errors/index-authentication) | ## Cause [#cause] The request comes from a browser origin that the key does not accept. ## What to do [#what-to-do] Add the origin to the key configuration, or move the call to your server: an API key must never be exposed in a browser. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # pack_in_use (/errors/pack_in_use) | Code | Type | HTTP | Category | | ------------- | ----------------------- | ---- | ---------------------------------- | | `pack_in_use` | `invalid_request_error` | 422 | [Products](/errors/index-products) | ## Cause [#cause] The pack is referenced by issued documents, so deleting it would break their composition. ## What to do [#what-to-do] Deactivate the pack instead of deleting it, or remove the product from the pack if that is what you meant to change. ## Related [#related] * [All Products error codes](/errors/index-products) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # pack_not_found (/errors/pack_not_found) | Code | Type | HTTP | Category | | ---------------- | ----------------- | ---- | ---------------------------------- | | `pack_not_found` | `not_found_error` | 404 | [Products](/errors/index-products) | ## Cause [#cause] The identifier does not resolve to any pack of the authenticated company. ## What to do [#what-to-do] List the packs and use the `id` returned there. ## Related [#related] * [All Products error codes](/errors/index-products) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # pack_share_link_failed (/errors/pack_share_link_failed) | Code | Type | HTTP | Category | | ------------------------ | ----------- | ---- | ---------------------------------- | | `pack_share_link_failed` | `api_error` | 500 | [Products](/errors/index-products) | ## Cause [#cause] The share link for the pack could not be produced. The pack itself is unaffected. ## What to do [#what-to-do] Retry in a few seconds and report the `request_id` if it keeps failing. ## Related [#related] * [All Products error codes](/errors/index-products) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid (/errors/parameter_invalid) | Code | Type | HTTP | Category | | ------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] A value object built from the payload rejected the value it received. `error.subcode` names which one — tax code, country code, rate, and so on. ## What to do [#what-to-do] Correct the field named in `error.param` following the format of the concept named by `error.subcode`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_boolean (/errors/parameter_invalid_boolean) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_boolean` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A parameter that must be a boolean received a value outside the accepted representations (`true`/`false`, `1`/`0`). ## What to do [#what-to-do] Send `true` or `false` in the parameter named by `error.param`. ## Message returned by the API [#message-returned-by-the-api] > The parameter must be a boolean value. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_cursor (/errors/parameter_invalid_cursor) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_cursor` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] The `starting_after` or `ending_before` cursor is not a valid UUID, so it cannot point at any row of the collection. ## What to do [#what-to-do] Use as cursor the `id` of the last object of the previous page (or the first one, for `ending_before`), copied verbatim from the response. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_empty (/errors/parameter_invalid_empty) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_empty` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A parameter arrived with an empty value: an `in` filter with no items, a comparison with nothing after the operator, or an equality filter with an empty string. ## What to do [#what-to-do] Send a non-empty value for the parameter in `error.param`, or drop the parameter from the request altogether. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_enum (/errors/parameter_invalid_enum) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_enum` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] The value falls outside the closed set the parameter accepts. On listings it also covers a filter operator other than `eq`, `gte`, `lte`, `gt`, `lt`, `in` or `contains`. ## What to do [#what-to-do] Pick one of the values documented for that parameter, or one of the supported filter operators. ## Message returned by the API [#message-returned-by-the-api] > The parameter value is not one of the allowed values. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_format (/errors/parameter_invalid_format) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_format` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] The value has the right type but not the shape the parameter requires: a date, an identifier pattern or a header such as `Factuarea-Version`. ## What to do [#what-to-do] Reformat the value of `error.param` to the pattern documented for it and retry. ## Message returned by the API [#message-returned-by-the-api] > The parameter format is invalid. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_integer (/errors/parameter_invalid_integer) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_integer` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A parameter that must be a whole number received something that cannot be parsed as one, such as `limit=abc`. ## What to do [#what-to-do] Send the parameter in `error.param` as a base-10 integer, with no decimals, thousand separators or quotes. ## Message returned by the API [#message-returned-by-the-api] > The parameter must be an integer. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_iso8601 (/errors/parameter_invalid_iso8601) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_iso8601` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A range filter (`gte`, `lte`, `gt`, `lt`) received a value that is neither numeric nor an ISO 8601 date. ## What to do [#what-to-do] Send dates as `YYYY-MM-DD`, or as full ISO 8601 with time zone (`2026-01-31T23:59:59Z`). ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_range (/errors/parameter_invalid_range) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_range` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A numeric parameter fell outside its accepted bounds. The usual case is `limit`, which must be between 1 and 100. ## What to do [#what-to-do] Send a value inside the documented bounds; to read more than 100 objects, paginate with `starting_after`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_string (/errors/parameter_invalid_string) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_string` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A parameter that must be text received an array, an object or a value that cannot be read as a string. ## What to do [#what-to-do] Send the parameter in `error.param` as a plain string. ## Message returned by the API [#message-returned-by-the-api] > The parameter must be a string. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_url (/errors/parameter_invalid_url) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_url` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] A field that must hold an absolute URL received a value that is not one, usually because the scheme or the host is missing. ## What to do [#what-to-do] Send an absolute `https://` URL in the field named by `error.param`. ## Message returned by the API [#message-returned-by-the-api] > The parameter must be a valid URL. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_uuid (/errors/parameter_invalid_uuid) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_uuid` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] An identifier field received a value that is not a valid UUID. Every v1 resource id is a UUID. ## What to do [#what-to-do] Use the `id` the API returned for that resource, copied verbatim; never an internal numeric id nor a truncated value. ## Message returned by the API [#message-returned-by-the-api] > The parameter must be a valid UUID. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_invalid_value (/errors/parameter_invalid_value) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_invalid_value` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] The value is syntactically correct but not admissible for this resource: outside the canonical catalogue of the field, or inconsistent with the rest of the payload. ## What to do [#what-to-do] Read `error.param` and `error.subcode`: together they name the field and the concrete rule the value broke. ## Message returned by the API [#message-returned-by-the-api] > The parameter value is invalid. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_missing (/errors/parameter_missing) | Code | Type | HTTP | Category | | ------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_missing` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] The endpoint requires a parameter that the request did not carry. `error.param` names it. ## What to do [#what-to-do] Add the parameter named in `error.param` to the query string or the body and retry the call. ## Message returned by the API [#message-returned-by-the-api] > A required parameter is missing. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # parameter_unknown (/errors/parameter_unknown) | Code | Type | HTTP | Category | | ------------------- | ----------------------- | ---- | -------------------------------- | | `parameter_unknown` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] The request carries a parameter the endpoint does not accept: a filter outside its allowlist, a `sort` field that is not sortable, or the offset-style `page` — v1 paginates by cursor. ## What to do [#what-to-do] Remove the parameter named in `error.param`; to page through a collection use `limit` together with `starting_after` or `ending_before`. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # payload_too_large (/errors/payload_too_large) | Code | Type | HTTP | Category | | ------------------- | ----------------------- | ---- | -------------------------------- | | `payload_too_large` | `invalid_request_error` | 413 | [Request](/errors/index-request) | ## Cause [#cause] The request body exceeds the accepted size: 1 MB as a rule, 6 MB on the endpoints that accept files. ## What to do [#what-to-do] Split the operation into smaller requests, or compress the attachment before sending it. ## Message returned by the API [#message-returned-by-the-api] > The payload exceeds the 1 MB limit. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # payment_method_invalid (/errors/payment_method_invalid) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ---------------------------------- | | `payment_method_invalid` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] Same closed allowlist as `invalid_payment_method`, reported when the value is rejected while reading the payment method field of the payload. ## What to do [#what-to-do] Send one of the seven accepted methods, lower-case and with underscores, as in `sepa_direct_debit`. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # payment_method_required (/errors/payment_method_required) | Code | Type | HTTP | Category | | ------------------------- | ------------------------ | ---- | ------------------------------------ | | `payment_method_required` | `payment_required_error` | 402 | [Companies](/errors/index-companies) | ## Cause [#cause] Adding a managed company charges a seat immediately, and the accounting firm operates in live mode with no payment method on file. ## What to do [#what-to-do] Open the billing portal at `error.details.payment_setup_url`, register a payment method and repeat the same call. ## Related [#related] * [All Companies error codes](/errors/index-companies) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # payout_reconciliation_amount_mismatch (/errors/payout_reconciliation_amount_mismatch) | Code | Type | HTTP | Category | | --------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `payout_reconciliation_amount_mismatch` | `invalid_request_error` | 422 | [Payments](/errors/index-payments) | ## Cause [#cause] The confirmed amount does not match the net amount of the payout, so the reconciliation would close with a difference nobody accounts for. ## What to do [#what-to-do] Reconcile against the net amount — gross minus Stripe fees — and check that the bank transaction corresponds to this payout. ## Related [#related] * [All Payments error codes](/errors/index-payments) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # pdf_generation_failed (/errors/pdf_generation_failed) | Code | Type | HTTP | Category | | ----------------------- | --------------------------- | ---- | ------------------------------ | | `pdf_generation_failed` | `service_unavailable_error` | 503 | [Server](/errors/index-server) | ## Cause [#cause] The rendering service could not produce the PDF. The document and its data are intact — what failed is the file. ## What to do [#what-to-do] Retry after a few seconds; if it persists, report the `request_id` to support, and meanwhile share the document through its public link. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # product_in_use (/errors/product_in_use) | Code | Type | HTTP | Category | | ---------------- | ----------------------- | ---- | ---------------------------------- | | `product_in_use` | `invalid_request_error` | 422 | [Products](/errors/index-products) | ## Cause [#cause] The product is referenced by issued documents or by other catalogue entries, and removing it would leave those references dangling. ## What to do [#what-to-do] Deactivate the product instead of deleting it: it stops being offered while the documents that used it stay intact. ## Related [#related] * [All Products error codes](/errors/index-products) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # product_not_found (/errors/product_not_found) | Code | Type | HTTP | Category | | ------------------- | ----------------- | ---- | ---------------------------------- | | `product_not_found` | `not_found_error` | 404 | [Products](/errors/index-products) | ## Cause [#cause] The identifier does not resolve to any product of the authenticated company. ## What to do [#what-to-do] Check the `id`, or look the product up by its SKU or its `external_id` before creating a duplicate. ## Related [#related] * [All Products error codes](/errors/index-products) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # profile_not_found (/errors/profile_not_found) | Code | Type | HTTP | Category | | ------------------- | ----------------- | ---- | -------------------------------- | | `profile_not_found` | `not_found_error` | 404 | [Request](/errors/index-request) | ## Cause [#cause] The `X-Active-Profile` header names a company that does not exist or does not belong to the accounting-firm tree of the authenticated key. Both cases answer the same so that the API never reveals companies of other tenants. ## What to do [#what-to-do] Send the `id` of one of the managed companies listed by `GET /v1/companies`, or drop the header to operate on your own company. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_already_accepted (/errors/proforma_already_accepted) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_already_accepted` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The customer already accepted the pro forma, and acceptance is registered once. ## What to do [#what-to-do] Move on to the conversion into an invoice; there is nothing left to accept. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_already_rejected (/errors/proforma_already_rejected) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_already_rejected` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The pro forma is already marked as rejected. ## What to do [#what-to-do] If the customer changed their mind, register the acceptance; a rejected pro forma can still be accepted. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_cannot_be_accepted (/errors/proforma_cannot_be_accepted) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_cannot_be_accepted` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] Acceptance does not apply from the current state: an invoiced, cancelled or expired pro forma no longer admits it. ## What to do [#what-to-do] Issue a new pro forma with the current conditions and get that one accepted. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_cannot_be_rejected (/errors/proforma_cannot_be_rejected) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_cannot_be_rejected` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] Rejection does not apply from the current state: once invoiced, cancelled or expired, the pro forma is closed. ## What to do [#what-to-do] If the operation is not going ahead and the pro forma was already invoiced, correct the invoice instead of rejecting the pro forma. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_cannot_be_sent (/errors/proforma_cannot_be_sent) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_cannot_be_sent` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] Sending by email does not apply to a pro forma in a terminal state: there is no live offer to deliver. ## What to do [#what-to-do] Issue a new pro forma and send that one; a closed document is only shared as a download. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_invalid_status_transition (/errors/proforma_invalid_status_transition) | Code | Type | HTTP | Category | | ------------------------------------ | ----------------------- | ---- | ------------------------------------ | | `proforma_invalid_status_transition` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The target status is unreachable from the current one: a draft can be accepted, cancelled or expire; an accepted pro forma can be invoiced, rejected or expire; invoiced, cancelled and expired are terminal. ## What to do [#what-to-do] Read the current `status` and go through the intermediate step the lifecycle requires. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_not_convertible_in_current_state (/errors/proforma_not_convertible_in_current_state) | Code | Type | HTTP | Category | | ------------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_not_convertible_in_current_state` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] Converting into an invoice requires the customer to have accepted the pro forma; from any other state there is no agreement to bill. ## What to do [#what-to-do] Register the acceptance first, then convert; if the customer never accepted, issue the invoice directly. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_not_deletable_in_current_state (/errors/proforma_not_deletable_in_current_state) | Code | Type | HTTP | Category | | ----------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] Only a draft pro forma can be deleted. Once it has been accepted, rejected or invoiced, it is part of the commercial trail. ## What to do [#what-to-do] Cancel the pro forma instead of deleting it; cancellation keeps the history and takes it out of circulation. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_not_draft (/errors/proforma_not_draft) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_not_draft` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The operation only makes sense while the pro forma is a draft, and this one has already moved on. ## What to do [#what-to-do] Read the `status` and use the operation matching it, or start from a new draft. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_not_editable_in_current_state (/errors/proforma_not_editable_in_current_state) | Code | Type | HTTP | Category | | ---------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_not_editable_in_current_state` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] Only a draft pro forma admits editing. Once it is accepted, rejected, expired, invoiced or cancelled, its content is settled. ## What to do [#what-to-do] Duplicate the pro forma to work on a new draft, instead of editing the one that is already closed. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_not_found (/errors/proforma_not_found) | Code | Type | HTTP | Category | | -------------------- | ----------------- | ---- | ------------------------------------ | | `proforma_not_found` | `not_found_error` | 404 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The identifier does not resolve to any pro forma of the authenticated company. ## What to do [#what-to-do] Check the `id` and the active profile, or look the pro forma up by its `external_id`. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # proforma_requires_at_least_one_line (/errors/proforma_requires_at_least_one_line) | Code | Type | HTTP | Category | | ------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `proforma_requires_at_least_one_line` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The pro forma has no lines, so there is no amount to put in front of the customer. ## What to do [#what-to-do] Add at least one line with description, quantity and unit price. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # public_link_expires_at_exceeds_max_days (/errors/public_link_expires_at_exceeds_max_days) | Code | Type | HTTP | Category | | ----------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `public_link_expires_at_exceeds_max_days` | `invalid_request_error` | 422 | [Proformas](/errors/index-proformas) | ## Cause [#cause] The requested expiry for the public link goes beyond the maximum window your plan allows for shared documents. ## What to do [#what-to-do] Send a nearer `expires_at`; when the link expires you can renew it as many times as you need. ## Related [#related] * [All Proformas error codes](/errors/index-proformas) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # purchase_invoice_already_exists (/errors/purchase_invoice_already_exists) | Code | Type | HTTP | Category | | --------------------------------- | ---------------- | ---- | ---------------------------------------------------- | | `purchase_invoice_already_exists` | `conflict_error` | 409 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] That supplier already has a purchase invoice registered with the same number. The pair supplier plus number identifies the document uniquely and prevents recording an expense twice. ## What to do [#what-to-do] Update the existing invoice instead of registering it again, or check the number if the supplier really issued two documents. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # purchase_invoice_not_deletable_in_current_state (/errors/purchase_invoice_not_deletable_in_current_state) | Code | Type | HTTP | Category | | ------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `purchase_invoice_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] Only draft and cancelled purchase invoices can be deleted. A pending or paid one is part of the expense ledger. ## What to do [#what-to-do] Cancel the invoice instead of deleting it; a cancelled invoice can then be removed if you really do not want it on record. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # purchase_invoice_not_draft (/errors/purchase_invoice_not_draft) | Code | Type | HTTP | Category | | ---------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `purchase_invoice_not_draft` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The operation only applies while the purchase invoice is a draft, and this one has already been registered. ## What to do [#what-to-do] Read `status` and use the operation matching it: registered invoices change through payment or cancellation, not through draft editing. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # purchase_invoice_not_editable_in_current_state (/errors/purchase_invoice_not_editable_in_current_state) | Code | Type | HTTP | Category | | ------------------------------------------------ | ----------------------- | ---- | ---------------------------------------------------- | | `purchase_invoice_not_editable_in_current_state` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] Only a draft purchase invoice can be edited. Once registered as pending, paid or cancelled, its content backs an accounting entry. ## What to do [#what-to-do] Move the invoice back to draft if it is still pending, or record the difference with a new document if it is already settled. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # purchase_invoice_not_found (/errors/purchase_invoice_not_found) | Code | Type | HTTP | Category | | ---------------------------- | ----------------- | ---- | ---------------------------------------------------- | | `purchase_invoice_not_found` | `not_found_error` | 404 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The identifier does not resolve to any purchase invoice of the authenticated company. ## What to do [#what-to-do] Check the `id` and the active profile, or look the invoice up by its `external_id`. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # purchase_invoice_requires_at_least_one_line (/errors/purchase_invoice_requires_at_least_one_line) | Code | Type | HTTP | Category | | --------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------- | | `purchase_invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Purchase Invoices](/errors/index-purchase-invoices) | ## Cause [#cause] The purchase invoice has no lines, so there is no expense nor deductible VAT to record. ## What to do [#what-to-do] Add at least one line with description, quantity and unit price before saving. ## Related [#related] * [All Purchase Invoices error codes](/errors/index-purchase-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # quote_already_accepted (/errors/quote_already_accepted) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ------------------------------ | | `quote_already_accepted` | `invalid_request_error` | 422 | [Quotes](/errors/index-quotes) | ## Cause [#cause] The quote was already approved, and approval is registered once. ## What to do [#what-to-do] Move on to the conversion into an invoice; there is nothing left to approve. ## Related [#related] * [All Quotes error codes](/errors/index-quotes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # quote_already_rejected (/errors/quote_already_rejected) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ------------------------------ | | `quote_already_rejected` | `invalid_request_error` | 422 | [Quotes](/errors/index-quotes) | ## Cause [#cause] The quote is already marked as rejected. ## What to do [#what-to-do] If the customer changed their mind, register the approval: a rejected quote can still be approved. ## Related [#related] * [All Quotes error codes](/errors/index-quotes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # quote_expired (/errors/quote_expired) | Code | Type | HTTP | Category | | --------------- | ----------------------- | ---- | ------------------------------ | | `quote_expired` | `invalid_request_error` | 422 | [Quotes](/errors/index-quotes) | ## Cause [#cause] The quote passed its validity date, so the offered conditions are no longer binding and it cannot be approved or converted as is. ## What to do [#what-to-do] Duplicate the quote with a new validity date and get the customer to approve the new one. ## Related [#related] * [All Quotes error codes](/errors/index-quotes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # quote_not_found (/errors/quote_not_found) | Code | Type | HTTP | Category | | ----------------- | ----------------- | ---- | ------------------------------ | | `quote_not_found` | `not_found_error` | 404 | [Quotes](/errors/index-quotes) | ## Cause [#cause] The identifier does not resolve to any quote of the authenticated company. ## What to do [#what-to-do] Check the `id` and the active profile, or look the quote up by its `external_id`. ## Related [#related] * [All Quotes error codes](/errors/index-quotes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # rate_limit_exceeded (/errors/rate_limit_exceeded) | Code | Type | HTTP | Category | | --------------------- | ------------------ | ---- | -------------------------------------- | | `rate_limit_exceeded` | `rate_limit_error` | 429 | [Rate Limit](/errors/index-rate-limit) | ## Cause [#cause] The key sent more requests than its rate allows in the current window. ## What to do [#what-to-do] Read the `Retry-After` header and wait that long; spread bulk work over time and use the bulk endpoints instead of one call per object. ## Message returned by the API [#message-returned-by-the-api] > You have exceeded the allowed request rate. Please retry later. ## Related [#related] * [All Rate Limit error codes](/errors/index-rate-limit) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # receipt_not_available (/errors/receipt_not_available) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ---------------------------------- | | `receipt_not_available` | `invalid_request_error` | 422 | [Payments](/errors/index-payments) | ## Cause [#cause] There is no receipt to issue because the document has no settled payment behind it. ## What to do [#what-to-do] Register the payment first; the receipt certifies a payment that already exists. ## Related [#related] * [All Payments error codes](/errors/index-payments) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # record_already_accepted (/errors/record_already_accepted) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ------------------------------------ | | `record_already_accepted` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] AEAT already accepted the record. Acceptance is terminal and its content is frozen as part of the fingerprint chain. ## What to do [#what-to-do] To correct an accepted invoice, issue a corrective invoice: an accepted record is never re-sent nor amended. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # record_immutable (/errors/record_immutable) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ------------------------------------ | | `record_immutable` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The record belongs to an append-only ledger: once written, its fiscal content is closed to changes and to deletion. ## What to do [#what-to-do] Add a new record that corrects it — an annulment plus a new registration, or a corrective invoice — instead of editing the existing one. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # record_not_rejected (/errors/record_not_rejected) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------------ | | `record_not_rejected` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The correction flow only applies to records AEAT rejected on data grounds. This record is in another state — a technical failure, for instance, is covered by the automatic retry. ## What to do [#what-to-do] Retry the transmission if the failure was technical; if AEAT accepted the record, correct the invoice with a corrective one. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # record_not_subsanable (/errors/record_not_subsanable) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ------------------------------------ | | `record_not_subsanable` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The record cannot be amended: it is not a registration record, or it has no source invoice from which its content could be regenerated. ## What to do [#what-to-do] Use an annulment plus a new registration, or issue a corrective invoice, depending on what has to change. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_already_active (/errors/recurring_already_active) | Code | Type | HTTP | Category | | -------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_already_active` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence is already running, so there is nothing to activate. Legacy code kept for compatibility: current endpoints report this as `recurring_invoice_already_active`. ## What to do [#what-to-do] Read `status` before acting; to change the schedule, update the recurrence instead of activating it again. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_already_active (/errors/recurring_invoice_already_active) | Code | Type | HTTP | Category | | ---------------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_already_active` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence is already running. ## What to do [#what-to-do] Read `status` before acting; to change when it runs next, update the recurrence. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_already_cancelled (/errors/recurring_invoice_already_cancelled) | Code | Type | HTTP | Category | | ------------------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_already_cancelled` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence was already cancelled, and cancellation is terminal. ## What to do [#what-to-do] Create a new recurrence if you need to bill this customer periodically again. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_already_paused (/errors/recurring_invoice_already_paused) | Code | Type | HTTP | Category | | ---------------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_already_paused` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence is already paused, so pausing it again changes nothing. ## What to do [#what-to-do] Read `status` before acting; to bring it back use the resume operation. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_cancelled_cannot_resume (/errors/recurring_invoice_cancelled_cannot_resume) | Code | Type | HTTP | Category | | ------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_cancelled_cannot_resume` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] A cancelled recurrence cannot be resumed: cancellation closes it for good, unlike a pause. ## What to do [#what-to-do] Duplicate it into a new recurrence, or pause instead of cancelling when the stop is meant to be temporary. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_cannot_run (/errors/recurring_invoice_cannot_run) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_cannot_run` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence cannot generate an invoice right now: it is not running, its cycle is over, or it lacks the data an invoice needs. `error.message` states the specific reason. ## What to do [#what-to-do] Fix what the message names — resume it, extend the number of occurrences, or complete the missing data — before forcing a run. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_has_generated_invoices (/errors/recurring_invoice_has_generated_invoices) | Code | Type | HTTP | Category | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_has_generated_invoices` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence already produced invoices, and those invoices depend on it for their traceability. ## What to do [#what-to-do] Cancel the recurrence instead of deleting it: it stops generating and the invoices already issued keep their origin. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_not_found (/errors/recurring_invoice_not_found) | Code | Type | HTTP | Category | | ----------------------------- | ----------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_not_found` | `not_found_error` | 404 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The identifier does not resolve to any recurrence of the authenticated company. ## What to do [#what-to-do] Check the `id` and the active profile, or look the recurrence up by its `external_id`. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_invoice_requires_at_least_one_line (/errors/recurring_invoice_requires_at_least_one_line) | Code | Type | HTTP | Category | | ---------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The recurrence has no lines, so every generated invoice would come out empty. ## What to do [#what-to-do] Add at least one line with description, quantity and unit price before saving or activating the recurrence. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # recurring_not_active (/errors/recurring_not_active) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ------------------------------------------------------ | | `recurring_not_active` | `invalid_request_error` | 422 | [Recurring Invoices](/errors/index-recurring-invoices) | ## Cause [#cause] The operation needs a running recurrence and this one is paused, completed or cancelled. Legacy code kept for compatibility with older integrations. ## What to do [#what-to-do] Resume the recurrence before the operation, or read `status` to see why it stopped. ## Related [#related] * [All Recurring Invoices error codes](/errors/index-recurring-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # register_sealing_failed (/errors/register_sealing_failed) | Code | Type | HTTP | Category | | ------------------------- | ----------- | ---- | ------------------------------ | | `register_sealing_failed` | `api_error` | 500 | [Server](/errors/index-server) | ## Cause [#cause] The cryptographic sealing of the record did not complete, so the closure was left unsigned rather than sealed with a broken signature. ## What to do [#what-to-do] Check the signing certificate of the company and repeat the closure; report the `request_id` if the failure repeats with a valid certificate. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # reminder_not_applicable (/errors/reminder_not_applicable) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `reminder_not_applicable` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The payment reminder does not apply: the invoice is not `sent` or `overdue`, there is no recipient email, the public link is missing or disabled, or another reminder went out in the last 24 hours. ## What to do [#what-to-do] Check the state, activate the public link, provide a recipient email, and respect the 24-hour cool-down before retrying. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # replay_delivery_not_retryable (/errors/replay_delivery_not_retryable) | Code | Type | HTTP | Category | | ------------------------------- | ----------------------- | ---- | ---------------------------------- | | `replay_delivery_not_retryable` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] Only failed deliveries can be replayed. A delivery that succeeded, or one still in flight, has nothing to resend. ## What to do [#what-to-do] Read the `status` of the delivery: replay applies to failed ones; for a successful delivery, read the event again instead. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # replay_event_expired (/errors/replay_event_expired) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `replay_event_expired` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The event behind the delivery was purged by the 30-day retention policy, so there is no payload left to resend. ## What to do [#what-to-do] Rebuild the state from the affected resource through its endpoint; events older than 30 days are not recoverable. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # report_format_invalid (/errors/report_format_invalid) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ---------------------------------------- | | `report_format_invalid` | `invalid_request_error` | 422 | [Tax Reports](/errors/index-tax-reports) | ## Cause [#cause] The format is outside the catalogue `txt_aeat`, `pdf`, `excel`. ## What to do [#what-to-do] Send `txt_aeat` to file with AEAT, `pdf` for a readable copy, or `excel` to work on the figures. ## Related [#related] * [All Tax Reports error codes](/errors/index-tax-reports) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # requires_annulment (/errors/requires_annulment) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | ------------------------------------ | | `requires_annulment` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The regenerated content changes a field that takes part in the fingerprint — issuer tax id, series and number, issue date, invoice type, tax amount or total — and the chain cannot be rewritten. ## What to do [#what-to-do] Annul the record and register a new invoice, or a corrective one, carrying the right data. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # resource_already_exists (/errors/resource_already_exists) | Code | Type | HTTP | Category | | ------------------------- | ---------------- | ---- | -------------------------------- | | `resource_already_exists` | `conflict_error` | 409 | [Request](/errors/index-request) | ## Cause [#cause] Creating the object would duplicate one that already exists under a unique key — tax id, SKU, external id. `error.details.existing_resource_id` points at the object that already holds the value. ## What to do [#what-to-do] Update the object returned in `existing_resource_id`, or send a different value in the field that must be unique. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # resource_conflict (/errors/resource_conflict) | Code | Type | HTTP | Category | | ------------------- | ---------------- | ---- | -------------------------------- | | `resource_conflict` | `conflict_error` | 409 | [Request](/errors/index-request) | ## Cause [#cause] The operation collided with the current state of the resource and no more specific conflict code applies. ## What to do [#what-to-do] Read the resource again, apply your change on top of the state you just read, and retry. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # resource_immutable (/errors/resource_immutable) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | -------------------------------- | | `resource_immutable` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] The object is closed to changes for this operation: its state or its accounting record forbids modifying it. ## What to do [#what-to-do] Read `error.subcode` to see which rule closed it; the way forward is usually issuing a new document instead of editing this one. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # resource_locked (/errors/resource_locked) | Code | Type | HTTP | Category | | ----------------- | ---------------- | ---- | -------------------------------- | | `resource_locked` | `conflict_error` | 409 | [Request](/errors/index-request) | ## Cause [#cause] Another operation holds the resource until it finishes: concurrent writes on the same object are serialised instead of interleaved. ## What to do [#what-to-do] Retry after a short back-off and reuse the same `Idempotency-Key`, so the retry cannot duplicate the write. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # resource_not_deletable (/errors/resource_not_deletable) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `resource_not_deletable` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] The object exists but its state or its dependants block the deletion. In bulk deletions this is the per-row code of every entry that could not be removed. ## What to do [#what-to-do] Read the `reason` of each failed row, remove or reassign the dependants, and retry the deletion only for those rows. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # resource_not_found (/errors/resource_not_found) | Code | Type | HTTP | Category | | -------------------- | ----------------- | ---- | -------------------------------- | | `resource_not_found` | `not_found_error` | 404 | [Request](/errors/index-request) | ## Cause [#cause] The identifier resolves to nothing visible to the authenticated company. Objects belonging to another company answer exactly the same way, by design. ## What to do [#what-to-do] Check the `id` and the active profile (`X-Active-Profile`); list the collection to confirm the object exists for this company. ## Message returned by the API [#message-returned-by-the-api] > The requested resource does not exist. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # route_not_found (/errors/route_not_found) | Code | Type | HTTP | Category | | ----------------- | ----------------- | ---- | -------------------------------- | | `route_not_found` | `not_found_error` | 404 | [Request](/errors/index-request) | ## Cause [#cause] The path does not match any v1 endpoint. It is usually a typo, a missing `/v1` prefix, or a path from a different area of the API. ## What to do [#what-to-do] Check the path in the API reference, base URL included (`https://api.factuarea.com/v1`). ## Message returned by the API [#message-returned-by-the-api] > Resource not found. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # scheduled_for_in_past (/errors/scheduled_for_in_past) | Code | Type | HTTP | Category | | ----------------------- | ----------------------- | ---- | ---------------------------------- | | `scheduled_for_in_past` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] `scheduled_for` is not strictly in the future, so there is no waiting period to reserve. ## What to do [#what-to-do] Send `scheduled_for` as an instant later than now, in ISO 8601 with time zone; to issue immediately, call the issuing operation instead. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # scope_not_allowed_by_plan (/errors/scope_not_allowed_by_plan) | Code | Type | HTTP | Category | | --------------------------- | --------------------- | ---- | -------------------------------------------- | | `scope_not_allowed_by_plan` | `authorization_error` | 422 | [Authorization](/errors/index-authorization) | ## Cause [#cause] One of the requested scopes belongs to a module that the plan does not include, so the key would be born with a permission that could never be exercised. ## What to do [#what-to-do] Issue the key without that scope, or upgrade the plan before including it. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # scope_not_allowed_in_sandbox (/errors/scope_not_allowed_in_sandbox) | Code | Type | HTTP | Category | | ------------------------------ | --------------------- | ---- | -------------------------------------------- | | `scope_not_allowed_in_sandbox` | `authorization_error` | 422 | [Authorization](/errors/index-authorization) | ## Cause [#cause] A test key cannot be born with scopes of modules vetoed in sandbox. ## What to do [#what-to-do] Remove those scopes from the test key, and keep them for the live key that will operate on the real company. ## Related [#related] * [All Authorization error codes](/errors/index-authorization) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # seat_charge_failed (/errors/seat_charge_failed) | Code | Type | HTTP | Category | | -------------------- | ------------------------ | ---- | ------------------------------------ | | `seat_charge_failed` | `payment_required_error` | 402 | [Companies](/errors/index-companies) | ## Cause [#cause] The immediate pro-rated charge for the seat was declined: the card was refused, it needs authentication, or the payment provider was unreachable. The company is not created if the seat is not paid. ## What to do [#what-to-do] Fix the payment method in the billing portal and retry the operation; check with your bank if the card keeps being declined. ## Related [#related] * [All Companies error codes](/errors/index-companies) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # send_failed (/errors/send_failed) | Code | Type | HTTP | Category | | ------------- | ----------- | ---- | ------------------------------ | | `send_failed` | `api_error` | 500 | [Server](/errors/index-server) | ## Cause [#cause] The document was not delivered by email: the mail provider rejected the message or was unreachable. ## What to do [#what-to-do] Check the recipient address and retry the send; the document itself is unaffected, only its delivery. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_already_archived (/errors/series_already_archived) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ------------------------------ | | `series_already_archived` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The series was already archived, and archiving is not repeated: a second call means the client is out of sync with the real state. ## What to do [#what-to-do] Read the series `is_archived` flag before acting; to bring it back use the unarchive operation. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_code_immutable_with_documents (/errors/series_code_immutable_with_documents) | Code | Type | HTTP | Category | | -------------------------------------- | ----------------------- | ---- | ------------------------------ | | `series_code_immutable_with_documents` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] Changing the prefix of a series that already issued documents would retroactively rewrite their fiscal identifier, while customers and AEAT hold the original number. ## What to do [#what-to-do] Create a new series with the new prefix and issue from it; the old one keeps the documents it already numbered. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_has_documents (/errors/series_has_documents) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ------------------------------ | | `series_has_documents` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The series already numbered documents, so it cannot be removed: the correlative sequence has to stay auditable. ## What to do [#what-to-do] Archive the series instead of deleting it — it stops being offered on new documents and keeps its history. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_immutable (/errors/series_immutable) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ------------------------------ | | `series_immutable` | `invalid_request_error` | 405 | [Series](/errors/index-series) | ## Cause [#cause] Series are not editable nor deletable through the API: legal numbering continuity requires their prefix, year and counter to stay put. ## What to do [#what-to-do] Create a new series with the values you need, and use the archive and unarchive operations to control which one is in play. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_initial_number_creates_gap (/errors/series_initial_number_creates_gap) | Code | Type | HTTP | Category | | ----------------------------------- | ----------------------- | ---- | ------------------------------ | | `series_initial_number_creates_gap` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The starting number jumps beyond the next natural correlative while documents already exist for the current year, and that gap in the sequence is not acceptable to AEAT. ## What to do [#what-to-do] Set the starting number to the next correlative, or open a new series if you need to start from a different point. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_locked_by_verifactu (/errors/series_locked_by_verifactu) | Code | Type | HTTP | Category | | ---------------------------- | ----------------------- | ---- | ------------------------------ | | `series_locked_by_verifactu` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] At least one invoice of the series holds a billing record accepted by AEAT, which freezes the prefix, the year and the numbering base of the series. ## What to do [#what-to-do] Create a new series for the change you need; only the name and the counter reset policy remain editable on this one. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_not_found (/errors/series_not_found) | Code | Type | HTTP | Category | | ------------------ | ----------------- | ---- | ------------------------------ | | `series_not_found` | `not_found_error` | 404 | [Series](/errors/index-series) | ## Cause [#cause] The identifier does not resolve to any numbering series of the authenticated company. ## What to do [#what-to-do] List the series, or look one up by its code stating the document type if the code repeats across types. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_type_invalid (/errors/series_type_invalid) | Code | Type | HTTP | Category | | --------------------- | ----------------------- | ---- | ------------------------------ | | `series_type_invalid` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The document type of the series is outside the catalogue `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. ## What to do [#what-to-do] Send one of the catalogue values: a series numbers exactly one type of document. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # series_year_locked (/errors/series_year_locked) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | ------------------------------ | | `series_year_locked` | `invalid_request_error` | 422 | [Series](/errors/index-series) | ## Cause [#cause] The series already issued documents in its current year. Moving the year would leave those documents pointing at an empty year while their taxable base sits in another. ## What to do [#what-to-do] Archive the series of the current year and create a new one for the target year. ## Related [#related] * [All Series error codes](/errors/index-series) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # service_unavailable (/errors/service_unavailable) | Code | Type | HTTP | Category | | --------------------- | --------------------------- | ---- | ------------------------------ | | `service_unavailable` | `service_unavailable_error` | 503 | [Server](/errors/index-server) | ## Cause [#cause] The service, or a dependency it needs, is temporarily unable to answer. ## What to do [#what-to-do] Retry with exponential back-off; do not change the payload, since the request itself is fine. ## Related [#related] * [All Server error codes](/errors/index-server) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # signature_payload_too_large (/errors/signature_payload_too_large) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ---------------------------------------------- | | `signature_payload_too_large` | `invalid_request_error` | 422 | [Delivery Notes](/errors/index-delivery-notes) | ## Cause [#cause] The signature image exceeds the accepted size for the field. ## What to do [#what-to-do] Send the signature as a PNG of the drawing area only, without upscaling it; a hand-drawn signature fits well under the limit. ## Related [#related] * [All Delivery Notes error codes](/errors/index-delivery-notes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # sii_excluded (/errors/sii_excluded) | Code | Type | HTTP | Category | | -------------- | ----------------------- | ---- | ------------------------------------ | | `sii_excluded` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The company is registered with SII, and SII filers are excluded from the VeriFactu regulation. ## What to do [#what-to-do] Keep reporting through SII; if the SII registration no longer reflects reality, correct it on the company before activating VeriFactu. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # simplified_invoice_cannot_be_substituted (/errors/simplified_invoice_cannot_be_substituted) | Code | Type | HTTP | Category | | ------------------------------------------ | ----------------------- | ---- | ---------------------------------- | | `simplified_invoice_cannot_be_substituted` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] One invoice of the substitution list cannot be replaced: it is not simplified, it is cancelled or annulled, it belongs to another company, or it already has a substitute. ## What to do [#what-to-do] Remove that invoice from the list — `error.message` names the number that blocks the batch — and send the substitution again. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # simplified_invoice_not_allowed (/errors/simplified_invoice_not_allowed) | Code | Type | HTTP | Category | | -------------------------------- | ----------------------- | ---- | ---------------------------------- | | `simplified_invoice_not_allowed` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The operation is not eligible for a simplified invoice: it exceeds EUR 3,000, or it is an intra-EU supply, an export, a reverse-charge operation, or the customer needs a full invoice to deduct VAT. ## What to do [#what-to-do] Issue a full F1 invoice identifying the recipient, or a substitute F3 if the simplified one was already issued. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # simplified_limit_exceeded (/errors/simplified_limit_exceeded) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ---------------------------------- | | `simplified_limit_exceeded` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The lines would push the simplified invoice (F2) over the absolute legal cap of EUR 3,000 VAT included. ## What to do [#what-to-do] Lower the amount, or issue a full invoice (F1) with the recipient fully identified. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # sku_already_exists (/errors/sku_already_exists) | Code | Type | HTTP | Category | | -------------------- | ---------------- | ---- | ---------------------------------- | | `sku_already_exists` | `conflict_error` | 409 | [Products](/errors/index-products) | ## Cause [#cause] Another product of the company already uses that SKU, and the SKU identifies the item uniquely in the catalogue. ## What to do [#what-to-do] Update the existing product — find it by SKU — or assign a different code to the new one. ## Related [#related] * [All Products error codes](/errors/index-products) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # stripe_payout_already_reconciled (/errors/stripe_payout_already_reconciled) | Code | Type | HTTP | Category | | ---------------------------------- | ----------------------- | ---- | ---------------------------------- | | `stripe_payout_already_reconciled` | `invalid_request_error` | 422 | [Payments](/errors/index-payments) | ## Cause [#cause] The payout was already reconciled, and reconciliation is terminal: repeating it would double-count the bank entry. ## What to do [#what-to-do] Read the payout to see the reconciliation on record; if it is wrong, correct the bank transaction it was matched against. ## Related [#related] * [All Payments error codes](/errors/index-payments) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # stripe_payout_not_found (/errors/stripe_payout_not_found) | Code | Type | HTTP | Category | | ------------------------- | ----------------- | ---- | ---------------------------------- | | `stripe_payout_not_found` | `not_found_error` | 404 | [Payments](/errors/index-payments) | ## Cause [#cause] The identifier does not resolve to any payout of the authenticated company. ## What to do [#what-to-do] List the payouts to get a current `id`; payouts appear once Stripe reports them, not at the moment of the charge. ## Related [#related] * [All Payments error codes](/errors/index-payments) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # suplido_line_cannot_carry_taxes (/errors/suplido_line_cannot_carry_taxes) | Code | Type | HTTP | Category | | --------------------------------- | ----------------------- | ---- | ---------------------------------- | | `suplido_line_cannot_carry_taxes` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The disbursement line carries charges of its own: a VAT rate, withholding, equivalence surcharge, discount, regime key, exemption cause or product/pack. A disbursement is not an operation of the issuer, so charging tax on it would mean paying tax on a supply you never made, and tying it to a product would move stock you never sold. ## What to do [#what-to-do] Leave `tax_rate`, `retention_rate`, `surcharge_rate` and `discount_percent` at zero — send `tax_rate: 0` explicitly, since omitting it applies the default 21 % — and drop `product_id`, `pack_id`, `regime_key` and `exemption_reason`; `error.details.offending_field` names the offending field. If the amount does carry your VAT, the line is `NORMAL`. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # suplido_not_allowed_in_simplified_invoice (/errors/suplido_not_allowed_in_simplified_invoice) | Code | Type | HTTP | Category | | ------------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `suplido_not_allowed_in_simplified_invoice` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The invoice is simplified (F2) and a simplified invoice does not identify the recipient. With no identified recipient there is nobody to evidence the payment on behalf of, so the amount cannot take disbursement treatment on this invoice type. ## What to do [#what-to-do] Issue a full invoice (F1) identifying the customer in order to include the disbursement, or keep the disbursement out of the simplified invoice and pass it on in a separate one. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # suplido_requires_source_invoice_reference (/errors/suplido_requires_source_invoice_reference) | Code | Type | HTTP | Category | | ------------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `suplido_requires_source_invoice_reference` | `invalid_request_error` | 422 | [Invoices](/errors/index-invoices) | ## Cause [#cause] The disbursement line does not carry `source_invoice_reference`, the number of the supporting document the third party issued in the customer's name. Without that document the payment is not evidenced as made on someone else's behalf, and the tax authority would treat it as the issuer's own taxable base, with VAT charged on it. ## What to do [#what-to-do] Add the number of the invoice or fee issued in the customer's name. If the supporting document is in your own name it is not a disbursement: invoice it as a `NORMAL` line with its VAT rate. ## Related [#related] * [All Invoices error codes](/errors/index-invoices) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # supplier_has_documents (/errors/supplier_has_documents) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ------------------------------------ | | `supplier_has_documents` | `invalid_request_error` | 422 | [Suppliers](/errors/index-suppliers) | ## Cause [#cause] The supplier is referenced by registered purchase invoices, and deleting it would leave those expenses without the party that issued them. ## What to do [#what-to-do] Deactivate the supplier instead of deleting it: it stops appearing in selectors and its invoices keep their reference. ## Related [#related] * [All Suppliers error codes](/errors/index-suppliers) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # supplier_not_found (/errors/supplier_not_found) | Code | Type | HTTP | Category | | -------------------- | ----------------- | ---- | ------------------------------------ | | `supplier_not_found` | `not_found_error` | 404 | [Suppliers](/errors/index-suppliers) | ## Cause [#cause] The identifier does not resolve to any supplier of the authenticated company. ## What to do [#what-to-do] Check the `id`, or look the supplier up by `tax_id` or `external_id` before creating a duplicate. ## Related [#related] * [All Suppliers error codes](/errors/index-suppliers) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # system_tax_default_modification_forbidden (/errors/system_tax_default_modification_forbidden) | Code | Type | HTTP | Category | | ------------------------------------------- | --------------------- | ---- | ---------------------------- | | `system_tax_default_modification_forbidden` | `authorization_error` | 403 | [Taxes](/errors/index-taxes) | ## Cause [#cause] Defaults of the shared catalogue taxes are not set on the tax itself: the catalogue is global and the preference belongs to your company. ## What to do [#what-to-do] Set the default through the company tax-defaults endpoint (`POST /v1/companies/me/tax-defaults`) instead of the tax endpoint. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # system_tax_immutable (/errors/system_tax_immutable) | Code | Type | HTTP | Category | | ---------------------- | ----------------------- | ---- | ---------------------------- | | `system_tax_immutable` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The tax belongs to the canonical AEAT catalogue shipped with the product. Its rate, code and name are fixed so that every company shares the same fiscal reference. ## What to do [#what-to-do] Create your own tax with the values you need, or use the operations that system taxes do allow: activating, deactivating and setting them as default. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # system_tax_immutable_field (/errors/system_tax_immutable_field) | Code | Type | HTTP | Category | | ---------------------------- | ----------------------- | ---- | ---------------------------- | | `system_tax_immutable_field` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The update touches a field that is frozen on a system tax; `error.param` names it. ## What to do [#what-to-do] Remove that field from the payload: on system taxes only the activation flag and the default flags can change. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # system_tax_undeletable (/errors/system_tax_undeletable) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ---------------------------- | | `system_tax_undeletable` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] System taxes are part of the shared fiscal catalogue and cannot be removed: deleting one would break the documents that reference it. ## What to do [#what-to-do] Deactivate the tax if you do not want it offered any more; deactivation also clears its default flags. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_applies_to_invalid (/errors/tax_applies_to_invalid) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ---------------------------- | | `tax_applies_to_invalid` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The scope of the tax is outside the catalogue `sale`, `purchase`, `both`. ## What to do [#what-to-do] Send `sale` for taxes charged on sales, `purchase` for those borne on purchases, or `both` when the tax applies on either side. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_code_already_exists (/errors/tax_code_already_exists) | Code | Type | HTTP | Category | | ------------------------- | ---------------- | ---- | ---------------------------- | | `tax_code_already_exists` | `conflict_error` | 409 | [Taxes](/errors/index-taxes) | ## Cause [#cause] Another tax of the catalogue already uses that code, and codes identify taxes unambiguously. ## What to do [#what-to-do] Reuse the existing tax, or pick a different code for the new one. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_id_already_exists (/errors/tax_id_already_exists) | Code | Type | HTTP | Category | | ----------------------- | ---------------- | ---- | -------------------------------- | | `tax_id_already_exists` | `conflict_error` | 409 | [Clients](/errors/index-clients) | ## Cause [#cause] Another client of the company already holds that tax id, and the tax id identifies the party uniquely inside a company. ## What to do [#what-to-do] Reuse the existing client — look it up by tax id — or correct the value if it was typed wrong. ## Related [#related] * [All Clients error codes](/errors/index-clients) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_id_required (/errors/tax_id_required) | Code | Type | HTTP | Category | | ----------------- | ----------------------- | ---- | ---------------------------- | | `tax_id_required` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The operation needs the tax identification number (NIF, CIF or NIE) of the party involved and the record does not carry one. ## What to do [#what-to-do] Fill in `tax_id` on the client, supplier or company before repeating the operation. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_in_use (/errors/tax_in_use) | Code | Type | HTTP | Category | | ------------ | ----------------------- | ---- | ---------------------------- | | `tax_in_use` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The tax is referenced by documents, products or suppliers. Removing it would leave historical documents without their fiscal reference. ## What to do [#what-to-do] Deactivate it instead of deleting it: it stops being offered on new documents and the existing ones keep their reference. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_inactive_cannot_be_default (/errors/tax_inactive_cannot_be_default) | Code | Type | HTTP | Category | | -------------------------------- | ----------------------- | ---- | ---------------------------- | | `tax_inactive_cannot_be_default` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] A deactivated tax cannot become the default, either globally or for a document type — it would offer a hidden default that no form can pick. ## What to do [#what-to-do] Activate the tax first, then set it as default. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_not_found (/errors/tax_not_found) | Code | Type | HTTP | Category | | --------------- | ----------------- | ---- | ---------------------------- | | `tax_not_found` | `not_found_error` | 404 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The identifier does not match any tax of the catalogue reachable by this company. ## What to do [#what-to-do] List the catalogue and use the `id` it returns; the tax may also be filtered out by the AEAT zone of your company. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_report_not_found (/errors/tax_report_not_found) | Code | Type | HTTP | Category | | ---------------------- | ----------------- | ---- | ---------------------------------------- | | `tax_report_not_found` | `not_found_error` | 404 | [Tax Reports](/errors/index-tax-reports) | ## Cause [#cause] The identifier does not resolve to any tax report of the authenticated company. ## What to do [#what-to-do] List the reports to get a current `id`, or generate the report for that period before reading it. ## Related [#related] * [All Tax Reports error codes](/errors/index-tax-reports) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_report_type_invalid (/errors/tax_report_type_invalid) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------------------- | | `tax_report_type_invalid` | `invalid_request_error` | 422 | [Tax Reports](/errors/index-tax-reports) | ## Cause [#cause] The report type is outside the catalogue `modelo_303`, `modelo_347`, `modelo_130`. ## What to do [#what-to-do] Send the model you need: 303 quarterly VAT, 130 quarterly personal income tax instalment, 347 annual third-party transactions. ## Related [#related] * [All Tax Reports error codes](/errors/index-tax-reports) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # tax_type_invalid (/errors/tax_type_invalid) | Code | Type | HTTP | Category | | ------------------ | ----------------------- | ---- | ---------------------------- | | `tax_type_invalid` | `invalid_request_error` | 422 | [Taxes](/errors/index-taxes) | ## Cause [#cause] The tax type is outside the catalogue `vat`, `retention`, `surcharge`, `other`. ## What to do [#what-to-do] Send one of the four types: it decides the allowed rate range and how the tax takes part in the totals. ## Related [#related] * [All Taxes error codes](/errors/index-taxes) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # timeout_seconds_out_of_range (/errors/timeout_seconds_out_of_range) | Code | Type | HTTP | Category | | ------------------------------ | ----------------------- | ---- | ---------------------------------- | | `timeout_seconds_out_of_range` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] `timeout_seconds` falls outside the range 1 to 30 seconds. ## What to do [#what-to-do] Send a value inside the range; if your receiver needs longer, acknowledge the event immediately and process it asynchronously on your side. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # too_many_auth_failures (/errors/too_many_auth_failures) | Code | Type | HTTP | Category | | ------------------------ | ---------------------- | ---- | ---------------------------------------------- | | `too_many_auth_failures` | `authentication_error` | 429 | [Authentication](/errors/index-authentication) | ## Cause [#cause] Too many failed authentication attempts arrived from the same address, so it is temporarily locked out to stop credential guessing. ## What to do [#what-to-do] Stop the retries, fix the key, and wait five minutes before trying again. ## Message returned by the API [#message-returned-by-the-api] > Too many failed authentication attempts from this IP. Wait 5 minutes before retrying. ## Related [#related] * [All Authentication error codes](/errors/index-authentication) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # too_many_custom_headers (/errors/too_many_custom_headers) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `too_many_custom_headers` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The endpoint declares more than 20 custom headers. ## What to do [#what-to-do] Keep the headers your receiver really needs; authentication usually fits in one. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # unknown_filter (/errors/unknown_filter) | Code | Type | HTTP | Category | | ---------------- | ----------------------- | ---- | -------------------------------- | | `unknown_filter` | `invalid_request_error` | 422 | [Request](/errors/index-request) | ## Cause [#cause] A listing received a filter it does not know. The canonical v1 parsers report this as `parameter_unknown`; this code survives for endpoints that have not migrated yet. ## What to do [#what-to-do] Remove the filter, or replace it with one of the fields the endpoint documents as filterable. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # unsupported_api_version (/errors/unsupported_api_version) | Code | Type | HTTP | Category | | ------------------------- | ----------------------- | ---- | -------------------------------- | | `unsupported_api_version` | `invalid_request_error` | 400 | [Request](/errors/index-request) | ## Cause [#cause] The `Factuarea-Version` header is well formed but names a version outside the supported set. ## What to do [#what-to-do] Send one of the supported version dates, or omit the header to use the version pinned to your API key. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # unsupported_format (/errors/unsupported_format) | Code | Type | HTTP | Category | | -------------------- | ----------------------- | ---- | ---------------------------------------- | | `unsupported_format` | `invalid_request_error` | 422 | [Tax Reports](/errors/index-tax-reports) | ## Cause [#cause] The requested format is not available for this model: not every filing produces every output. ## What to do [#what-to-do] Ask for one of the formats the model does offer — the AEAT text file, the PDF or the spreadsheet. ## Related [#related] * [All Tax Reports error codes](/errors/index-tax-reports) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # unsupported_media_type (/errors/unsupported_media_type) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | -------------------------------- | | `unsupported_media_type` | `invalid_request_error` | 415 | [Request](/errors/index-request) | ## Cause [#cause] A request with a body declared a `Content-Type` other than `application/json`. ## What to do [#what-to-do] Set `Content-Type: application/json` and serialise the body as JSON. ## Message returned by the API [#message-returned-by-the-api] > Only Content-Type application/json is accepted. ## Related [#related] * [All Request error codes](/errors/index-request) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # verifactu_already_submitted (/errors/verifactu_already_submitted) | Code | Type | HTTP | Category | | ----------------------------- | ----------------------- | ---- | ------------------------------------ | | `verifactu_already_submitted` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The invoice already has its registration record. Exactly one registration exists per invoice, so a second one would break the idempotency of the chain. ## What to do [#what-to-do] Read the existing record instead of creating another; to change what was reported, issue a corrective invoice. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # verifactu_mode_invalid (/errors/verifactu_mode_invalid) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ------------------------------------ | | `verifactu_mode_invalid` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The mode is outside the catalogue `verifactu` / `no_verifactu`. ## What to do [#what-to-do] Send `verifactu` to report to AEAT in real time, or `no_verifactu` for the local record mode. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # verifactu_not_eligible (/errors/verifactu_not_eligible) | Code | Type | HTTP | Category | | ------------------------ | ----------------------- | ---- | ------------------------------------ | | `verifactu_not_eligible` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The invoice cannot be registered with AEAT right now: the company is not on VeriFactu mode, it has no active certificate, or the certificate is revoked or issued for a different tax id. ## What to do [#what-to-do] Activate VeriFactu mode and upload a valid FNMT certificate matching the company tax id; records deferred for this reason are re-queued as soon as a valid certificate is in place. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # verifactu_record_not_found (/errors/verifactu_record_not_found) | Code | Type | HTTP | Category | | ---------------------------- | ----------------- | ---- | ------------------------------------ | | `verifactu_record_not_found` | `not_found_error` | 404 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The identifier does not match any billing record of the authenticated company. ## What to do [#what-to-do] Check the `id`, or locate the record by its CSV, its fingerprint or the number of the invoice that produced it. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # verifactu_transmission_failed (/errors/verifactu_transmission_failed) | Code | Type | HTTP | Category | | ------------------------------- | ----------------------- | ---- | ------------------------------------ | | `verifactu_transmission_failed` | `invalid_request_error` | 422 | [VeriFactu](/errors/index-verifactu) | ## Cause [#cause] The transmission of the record to AEAT did not complete: the endpoint was unreachable or answered with an incident. ## What to do [#what-to-do] Check the state of the record — transmission retries automatically with exponential back-off — and force a retry once the waiting window has elapsed. ## Related [#related] * [All VeriFactu error codes](/errors/index-verifactu) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # webhook_delivery_not_found (/errors/webhook_delivery_not_found) | Code | Type | HTTP | Category | | ---------------------------- | ----------------- | ---- | ---------------------------------- | | `webhook_delivery_not_found` | `not_found_error` | 404 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The identifier does not match any delivery attempt, or the delivery falls outside the retention window kept for the history. ## What to do [#what-to-do] List the deliveries of the endpoint to get a current `id`; deliveries older than the retention window are no longer available. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # webhook_endpoint_degraded (/errors/webhook_endpoint_degraded) | Code | Type | HTTP | Category | | --------------------------- | ----------------------- | ---- | ---------------------------------- | | `webhook_endpoint_degraded` | `invalid_request_error` | 422 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The endpoint is degraded after repeated delivery failures, so test pings are refused while it stays in that state. ## What to do [#what-to-do] Fix the receiver, reactivate the endpoint, and only then send the test ping. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # webhook_endpoint_not_found (/errors/webhook_endpoint_not_found) | Code | Type | HTTP | Category | | ---------------------------- | ----------------- | ---- | ---------------------------------- | | `webhook_endpoint_not_found` | `not_found_error` | 404 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The identifier does not resolve to any webhook endpoint of the authenticated company. ## What to do [#what-to-do] List your endpoints and use the `id` returned there. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # webhook_secret_recently_rotated (/errors/webhook_secret_recently_rotated) | Code | Type | HTTP | Category | | --------------------------------- | ------------------ | ---- | ---------------------------------- | | `webhook_secret_recently_rotated` | `rate_limit_error` | 429 | [Webhooks](/errors/index-webhooks) | ## Cause [#cause] The signing secret was rotated less than five minutes ago. The grace window lets your receiver accept both secrets during the switch; rotating again inside it would invalidate signatures still in flight. ## What to do [#what-to-do] Wait five minutes from the last rotation, and deploy the new secret in your receiver before rotating again. ## Related [#related] * [All Webhooks error codes](/errors/index-webhooks) * [Error codes by category](/errors) * [Full reference table](/guides/errors/all) * [Error model](/guides/errors) --- # API de Factuarea (/es) La API REST de Factuarea expone recursos de facturación (clientes, productos, facturas, presupuestos, facturas proforma, albaranes, facturas recurrentes, facturas de compra) sobre HTTPS con autenticación por **API key**. Toda la superficie pública vive en [`https://api.factuarea.com/v1`](https://api.factuarea.com/v1) y devuelve JSON. Cada recurso se identifica por un `id` opaco (un string UUID v7). <Cards> <Card icon="<Rocket />" title="Inicio rápido — tu primera factura en 5 minutos" href="/guides/quickstart"> Una secuencia de copiar y pegar contra una clave `fact_test_`: verifica tu clave, obtén una serie y un impuesto, crea un cliente, emite una factura y envíala. </Card> </Cards> ## Inicio rápido [#inicio-rápido] <Steps> <Step> **La API viene con tu plan** La API pública está **incluida en todos los planes de Factuarea** — sin programa beta ni add-on aparte. Durante el trial de 10 días ya tienes acceso a la API con el tier `free`; los planes de pago suben el tier de rate limit. Consulta [Límites de peticiones](/guides/rate-limits). </Step> <Step> **Crea tu primera API key** Abre [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) y crea una clave con los scopes que necesites (por ejemplo `invoices:read,clients:read` para empezar). Copia el secreto **una sola vez** — no podrás volver a verlo. Elige el entorno **Test** para obtener una clave `fact_test_` que opera sobre un sandbox aislado sin efectos en el mundo real. Crea contra él primero y luego crea una clave `fact_live_` para pasar a producción. Consulta [Modo de prueba y sandbox](/guides/test-mode). </Step> <Step> **Verifica tu clave** Antes que nada, confirma que la clave funciona. `GET /v1/account` introspecciona la credencial — devuelve la empresa a la que pertenece, el plan, y los **scopes** y el **tier** de límite de peticiones de la propia clave (necesita `account:read`): ```bash curl https://api.factuarea.com/v1/account \ -H "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` ✅ Deberías ver un `200` con una instantánea de `account`: ```json { "data": { "object": "account", "company": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Acme Soluciones SL", "tax_id": "B12345678" }, "plan": { "slug": "empresario", "name": "Empresario" }, "api_key": { "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "name": "Sandbox integration", "prefix": "fact_test_3pXnR2Vb", "scopes": ["account:read", "clients:read", "invoices:read"], "tier": "starter" } } } ``` Si obtienes `401 invalid_api_key`, vuelve a comprobar el valor. El array `scopes` te dice exactamente qué puede hacer esta clave — una llamada posterior que falle con `403 insufficient_scope` carece de alguno de ellos. </Step> <Step> **Haz tu primera petición de datos** Ahora lista un recurso real. `GET /v1/clients` devuelve un envoltorio estándar con `data` (resultados), `has_more` y `next_cursor` ([paginación por cursor](/guides/pagination)): ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` ¿Listo para emitir tu primera factura de principio a fin? Sigue el [Inicio rápido](/guides/quickstart). Si recibes un error, búscalo en [Errores](/guides/errors) por el `code` devuelto. </Step> <Step> **Configura webhooks (opcional)** Si tu integración necesita reaccionar a eventos (factura pagada, presupuesto aceptado, etc.), configura un webhook endpoint firmado con HMAC SHA256. Consulta [Webhooks](/guides/webhooks). </Step> </Steps> ## Qué cubre la API [#qué-cubre-la-api] <Cards> <Card icon="<Users />" title="Clientes y proveedores" href="/api-reference/clients/public-api.v1.clients.list"> CRUD completo, búsqueda por tax ID, validación VIES. </Card> <Card icon="<Box />" title="Catálogo" href="/api-reference/products/public-api.v1.products.list"> Productos con precios, stock, SKU y tipos impositivos. </Card> <Card icon="<FileText />" title="Documentos de venta" href="/api-reference/invoices/public-api.v1.invoices.list"> Facturas, presupuestos, facturas proforma, albaranes, facturas recurrentes — con líneas, retenciones y recargo de equivalencia. </Card> <Card icon="<Zap />" title="Acciones de documento" href="/api-reference/invoices/public-api.v1.invoices.send"> Enviar por email, marcar como pagada/aceptada, generar PDF, anular, crear factura rectificativa, convertir entre tipos. </Card> <Card icon="<Receipt />" title="Compras" href="/api-reference/purchase-invoices/public-api.v1.purchase_invoices.list"> Facturas de proveedor con subida de PDF, mark\_paid, mark\_received. </Card> <Card icon="<Hash />" title="Series de documentos" href="/api-reference/series/public-api.v1.series.list"> Series de numeración legal por tipo de documento (de solo lectura vía API para garantizar la continuidad fiscal). </Card> <Card icon="<Landmark />" title="Facturación FACe (B2G)" href="/guides/face-invoicing"> Descarga del XML FacturaE 3.2.2 y envíos a FACe — envía, sigue el estado de tramitación y solicita anulaciones. </Card> <Card icon="<Clock />" title="Control horario" href="/guides/workforce-overview"> Empleados, horarios de trabajo, el registro de fichajes, cierres mensuales, ausencias, presencia y festivos — el registro de jornada del RD-ley 8/2019. </Card> <Card icon="<Plug />" title="Servidor MCP para agentes de IA" href="/mcp"> Toda la API como <Stat n="tools" /> herramientas Model Context Protocol, con OAuth 2.1 y autenticación por API key — conecta Claude y otros agentes en segundos. </Card> </Cards> ## Diseño del contrato [#diseño-del-contrato] La API sigue los patrones que esperarías de un proveedor moderno: * **Identificadores opacos** — la clave `id` lleva un string UUID v7 en lugar de un entero incremental. Consulta [Paginación](/guides/pagination) para la semántica del cursor. * **Errores normalizados** — cada error devuelve un envoltorio con `type`, `code`, `message`, `param`, `doc_url` y `request_id`. Consulta [Errores](/guides/errors). * **Idempotency keys** — soportadas en cada `POST` para evitar duplicados en los reintentos. Consulta [Idempotencia](/guides/idempotency). * **Límites de peticiones por tier** — cuotas por minuto y mensuales, con cabeceras `X-RateLimit-*` en cada respuesta. Consulta [Límites de peticiones](/guides/rate-limits). * **Versionado por URL** — `/v1/*`. Los cambios incompatibles disparan `/v2/*` con una política de deprecación documentada. Consulta [Versionado](/guides/versioning). * **Webhooks con rotación de doble secreto** — HMAC SHA256, reintento exponencial con hasta 8 intentos. Consulta [Webhooks](/guides/webhooks). ## SDKs [#sdks] Ofrecemos [SDKs oficiales de TypeScript y PHP](/sdks) (`@factuarea/sdk` y `factuarea/factuarea-php`) con reintentos, idempotencia, paginación por cursor, errores tipados y verificación de webhooks integrados. Si tu lenguaje no está cubierto, cualquier cliente HTTP estándar (curl, Postman, axios, requests, Guzzle) funciona — la API es REST plano sobre JSON. <Callout type="info"> La API REST pública complementa el cliente web de Factuarea ([`app.factuarea.com`](https://app.factuarea.com)) — no lo reemplaza. Las operaciones que la API no expone (gestión de planes, branding, configuración fiscal global de la empresa) siguen viviendo en la app. </Callout> --- # Launch (/es/changelog/launch) ## Control horario — 2026-07-11 [#control-horario--2026-07-11] Factuarea ya cubre el deber del empleador español de llevar un registro diario de jornada — **RD-ley 8/2019**, art. 34.9 del Estatuto de los Trabajadores — y expone todo el sistema de personal sobre el mismo contrato v1. Es el **VeriFactu del control horario**: un ledger de sola adición sellado por una cadena de hash SHA-256 por empresa, donde nada se edita ni se borra y cualquier manipulación rompe la cadena. Toda la superficie está protegida por el nuevo **módulo `control_horario`**. Empieza por el [resumen de control horario](/guides/workforce-overview). * **Ocho dominios nuevos** — empleados (con invitaciones y facturación por asiento), horarios de trabajo, fichajes (entrada/salida, pausas, fichajes retroactivos y correcciones), cierres mensuales del registro, exportaciones para nóminas, ausencias (tipos, políticas, solicitudes, saldos y calendario), presencia y festivos. * **Scopes nuevos** — un conjunto dedicado dentro del catálogo cerrado: `employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read` y `payroll_exports:read`, todos tras el módulo `control_horario`. Consulta [Scopes e irreversibilidad](/guides/scopes-and-irreversibility). * **Cierre mensual sellado** — congela un mes finalizado y séllalo con una firma RSA-SHA256 desacoplada sobre la instantánea; el sellado es irreversible (uno por cierre) y verificable de forma independiente. Exporta el registro diario en el formato `rdley_8_2019`, o un fichero de incidencias para nóminas A3, Sage o NominaSOL. Consulta [Cierre mensual del registro](/guides/monthly-time-close). * **Rol de empleado solo en el portal** — un empleado ficha, sigue un horario y solicita ausencias desde el portal, y **nunca** cuenta contra el límite `users` del plan. * **Add-on por asiento** — los empleados se facturan mediante una suscripción mensual dedicada (`employee-seats`) cuya cantidad sigue tu censo activo; contratarla activa el módulo. Una cuenta enterprise facturada por contrato lo obtiene gratis. Consulta [Facturación de asientos de empleado](/guides/employee-seats). * **Paridad MCP** — cada ruta v1 refleja una tool MCP pública, así que un agente ejecuta las mismas operaciones. Consulta el [catálogo de tools MCP](/mcp/tools#employee). <Callout type="info"> Dos dominios son **de solo lectura** vía API — presencia y festivos exponen solo lecturas. Declarar presencia en oficina o remoto y crear festivos locales propios son tareas solo del portal, sin scope `presence:write` ni `holidays:write`. </Callout> ## API y MCP incluidos en todos los planes — 2026-07-04 [#api-y-mcp-incluidos-en-todos-los-planes--2026-07-04] La API pública y el servidor MCP dejan de venderse como add-on `developer_api` aparte — ahora están **incluidos en todos los planes de Factuarea**: * **Tier por plan** — tu tier de rate limit se deriva de tu plan: Emprendedor → `starter` (30 req/min, 5.000 req/mes), Empresario → `pro` (300 req/min, 50.000 req/mes), Enterprise → `scale` (personalizado, sin topes). Consulta [Límites de peticiones](/guides/rate-limits). * **Trial incluido** — durante el trial de 10 días tienes acceso a la API con el tier `free` (10 req/min, 100 req/mes). * **Boost de capacidad** — si necesitas más capacidad sin cambiar de plan, suscríbete desde el dashboard a un tier estrictamente superior al que otorga tu plan; un tier igual o inferior devuelve `422 boost_not_applicable`. Consulta [Boost de capacidad](/guides/rate-limits#capacity-boost). * **El add-on desaparece** — los add-ons de developer Starter y Pro dejan de venderse. El código de error `addon_not_active` se mantiene (ahora significa que la empresa no tiene un plan activo que incluya acceso a la API), así que las integraciones existentes no necesitan ningún cambio. * **Programa beta cerrado** — el acceso a la API ya no se solicita: crea una key desde [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) y empieza a llamar a `/v1`. <Callout type="info"> **v1** — publicada el 2026-05-03. Este es el primer lanzamiento público de la plataforma de Factuarea; todo lo que sigue se publica junto. Los próximos lanzamientos se añaden a esta página, del más reciente al más antiguo, cada uno encabezado por su versión y fecha. </Callout> Por primera vez puedes integrar Factuarea con cualquier sistema externo — por código, por SDK, por línea de comandos o por agente de IA — sin scraping ni macros. La superficie pública es un único contrato en `https://api.factuarea.com/v1`, accesible de cuatro formas: la API REST, los SDKs de TypeScript y PHP, el CLI `factuarea` y el servidor MCP. Cada superficie habla con los mismos recursos y aplica los mismos scopes. ## REST API v1 [#rest-api-v1] La API REST pública expone **<Stat n="operations" /> operaciones en <Stat n="resources" /> recursos** como JSON plano sobre HTTPS. Cada recurso se identifica por una clave `id` opaca (un UUID v7). ### Documentos de venta [#documentos-de-venta] * **Facturas** (`/v1/invoices`) — CRUD completo y el ciclo de vida completo: enviar, marcar como pagada, cancelar, anular, duplicar, PDF y enlace público, cobros y recibos, recordatorios. Facturas rectificativas con los códigos de motivo de rectificación `R1`–`R5`, elegibilidad y sustitución de factura simplificada, emisión programada (schedule / reschedule / unschedule) y exportación trimestral (ZIP y email). Creación, envío, cambio de estado, borrado y PDF en lote, además de exportación a Excel. * **Presupuestos** (`/v1/quotes`) — CRUD + aceptar, rechazar, convertir a factura, PDF, enlace público. * **Facturas proforma** (`/v1/proformas`) — CRUD + convertir a factura, PDF, enlace público. * **Albaranes** (`/v1/delivery_notes`) — CRUD + firmar, marcar como entregado, convertir a factura. * **Facturas recurrentes** (`/v1/recurring_invoices`) — CRUD + activar, pausar, reanudar, cancelar y previsualizar la próxima ejecución. ### Compras [#compras] * **Facturas de compra** (`/v1/purchase_invoices`) — CRUD con adjunto PDF, marcar como pagada, registro de pagos e informes de pendientes / vencidas. ### CRM y catálogo [#crm-y-catálogo] * **Clientes** (`/v1/clients`) — CRUD completo, búsqueda por NIF/CIF, verificación censal de la AEAT y VIES, e importación CSV con plantilla descargable. * **Proveedores** (`/v1/suppliers`) — CRUD completo, búsqueda por NIF/CIF. * **Productos** (`/v1/products`) — CRUD, búsqueda por SKU o external id, control de stock (fijar, ajustar y actualización en lote), informe de stock bajo, analítica de ventas, e imágenes de galería y vídeo. * **Series de documentos** (`/v1/series`) — series de numeración legal por tipo de documento, con reinicio mensual / anual, selección de predeterminada y archivar / desarchivar. * **Impuestos** (`/v1/taxes`) — tipos impositivos (IVA, retención de IRPF, recargo de equivalencia) con predeterminados por documento. ### Cumplimiento fiscal español [#cumplimiento-fiscal-español] * **VeriFactu** (`/v1/verifactu/*`, `/v1/invoices/{invoice}/verifactu`) — registros de facturación, la cadena de huellas del SIF y su validación, subsanación (registros de corrección), la declaración responsable y su histórico, y la gestión de certificados FNMT. * **FacturaE / FACe** (`/v1/invoices/{invoice}/facturae`, `/v1/face-submissions`) — descarga del XML FacturaE 3.2.2 y envíos B2G a las administraciones públicas mediante FACe (enviar, seguir, anular). * **Censo de la AEAT** (`/v1/account/census-verification`, `/v1/clients/*`) — verifica un NIF/CIF contra el registro de la AEAT. * **Informes fiscales** (`/v1/tax_reports/*`) — genera, previsualiza, descarga y mantén el histórico de los Modelos 303 (IVA), 347 (operaciones anuales con terceros) y 130 (pago fraccionado de IRPF). ### Pagos [#pagos] * **Autofacturación de Stripe** (`/v1/stripe-autoinvoicing/*`) — conecta cuentas de Stripe y emite facturas automáticamente a partir de los pagos de Stripe, incluidas facturas rectificativas automáticas en las devoluciones. * **Payouts y conciliación** (`/v1/payouts`, `/v1/connected-accounts`) — lee los payouts de Stripe y concilia las liquidaciones, con soporte de extractos bancarios Norma 43. ### Empresas gestionadas (gestorías) [#empresas-gestionadas-gestorías] * **Empresas** (`/v1/companies`) — aprovisiona y opera empresas hijas desde una cuenta maestra: crear, activar, desactivar, seguir el estado de creación y emitir API keys por empresa (crear, rotar, revocar). Previsualiza el coste por seat antes de confirmar con `/v1/companies/seat-charge-preview`. Opera en nombre de una hija en una sola petición con el header `X-Active-Profile`. ### Webhooks y eventos [#webhooks-y-eventos] * **Webhooks** (`/v1/webhook_endpoints` con `deliveries` anidados) — endpoints suscribibles firmados con HMAC SHA256, rotación de doble secreto, ping / test, y un histórico de entregas que puedes reenviar. * **Eventos** (`/v1/events`, `/v1/event-catalog`) — el flujo histórico de eventos y el catálogo de tipos de evento suscribibles. ### Cuenta [#cuenta] * **Cuenta** (`/v1/account`) — introspecciona la credencial autenticada (empresa, plan, scopes y tier de límite de peticiones), gestiona API keys, personaliza las plantillas de documento y ejecuta tu propia verificación censal. ## Fundamentos de la API [#fundamentos-de-la-api] Comportamiento que comparten todos los recursos, así una integración lo aprende una sola vez: * **Modo de prueba** — las claves `fact_test_*` se ejecutan contra una empresa sandbox aislada; los efectos externos (VeriFactu/AEAT, FACe, email, webhooks) no se ejecutan, así creas y pruebas sin tocar los datos de producción. * **Identificadores opacos** — cada recurso expone una clave `id` cuyo valor es un UUID v7, con foreign keys como `*_id`. * **Paginación por cursor** — `starting_after` / `ending_before`, sin `?page=`. * **Idempotencia** — el header `Idempotency-Key` (máx. 64 caracteres, TTL de 24 h); una petición repetida devuelve la respuesta original almacenada — incluida una `4xx` cacheada — marcada con `Idempotent-Replayed`. * **Límites de peticiones** — cuotas por tier, por minuto y mensuales, con headers `X-RateLimit-*`. * **Errores normalizados** — el envoltorio `{ error: { type, code, message, param, request_id, doc_url } }`; los errores de validación señalan el campo problemático mediante `param`. Ramifica según `code`, nunca según el `message` orientado a personas. * **Operaciones en lote** — los endpoints por lotes informan del éxito parcial por elemento, así una fila incorrecta no hace fallar toda la petición. * **Importación y exportación** — importación CSV de clientes (con plantilla descargable) y exportación de facturas a Excel. * **Webhooks firmados** — HMAC SHA256 con ±5 min de tolerancia y reintentos exponenciales hasta 8 intentos. * **Scopes** — un catálogo cerrado `resource:action`; toda operación a la que no puedes acceder queda oculta, y los scopes destructivos `write` / `delete` se marcan como sensibles en la pantalla de consentimiento de OAuth y nunca se pre-marcan. * **Versionado** — el prefijo de URL `/v1` más un header `Factuarea-Version` fijado. `/v1` se mantiene estable durante al menos 24 meses; cualquier breaking change vive en `/v2` con una ventana de coexistencia de al menos 12 meses. ## SDKs oficiales — TypeScript y PHP [#sdks-oficiales--typescript-y-php] Los SDKs mantenidos envuelven toda la API REST v1 con un runtime premium, así no escribes HTTP a mano. Consulta la [sección de SDKs](/sdks). * **TypeScript / Node.js** — [`@factuarea/sdk`](https://www.npmjs.com/package/@factuarea/sdk) en npm. ESM + CommonJS dual, declaraciones de tipos completas, Node 20+ (y Deno / Bun / Workers). Código fuente: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). * **PHP** — [`factuarea/factuarea-php`](https://packagist.org/packages/factuarea/factuarea-php) en Packagist. PSR-4, basado en Guzzle, PHP 8.2+. Código fuente: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). ```bash npm install @factuarea/sdk composer require factuarea/factuarea-php ``` Ambos comparten el mismo runtime: reintentos automáticos (con backoff, respetando `Retry-After`), claves de idempotencia automáticas, auto-paginación por cursor, una jerarquía de [errores](/guides/errors) tipada, verificación de webhooks en tiempo constante y descargas binarias (PDF). Cada página de la referencia de la API muestra un snippet de TypeScript, PHP y cURL listo para copiar. Cada release fija una [`Factuarea-Version`](/guides/versioning) y la envía en cada request. ## Interfaz de línea de comandos [#interfaz-de-línea-de-comandos] El [CLI `factuarea` oficial](/cli) (`v0.1.3`) opera toda la superficie v1 desde tu terminal. Es **agent-first** — salida JSON estable, exit codes semánticos y descubrimiento en una sola llamada — y el árbol de comandos se genera desde el spec OpenAPI, así que nunca se desincroniza de la superficie en vivo. * **Una clave, dos entornos** — el prefijo de la clave selecciona el entorno; una mutación `fact_live_` requiere además el flag explícito `--live` como red de seguridad. * **Devloop** — `listen` reenvía eventos a tu máquina y `trigger` produce eventos reales de sandbox, así pruebas webhooks en local sin túnel ni ngrok. * **Instalación** — Homebrew, npm o un instalador `curl`. Consulta el [CLI](/cli). ## Servidor MCP para agentes de IA [#servidor-mcp-para-agentes-de-ia] El [servidor MCP](/mcp) en `https://mcp.factuarea.com` expone la API pública como **<Stat n="tools" /> tools de Model Context Protocol** sobre el transporte **Streamable HTTP**, así los agentes de IA (Claude y otros) las descubren y las llaman sin que tengas que cablear cada endpoint. * **Dos canales de auth** — una **API key** (`fact_live_` / `fact_test_`) para el propietario de la cuenta (hasta las <Stat n="tools" /> tools), u **OAuth 2.1** para apps de terceros (un catálogo curado de <Stat n="oauth_reachable" /> tools). Consulta [Conectar un cliente](/mcp/connect#authenticate). * **OAuth 2.1 completo** — Dynamic Client Registration (RFC 7591), PKCE (S256), una pantalla de consentimiento con selección de empresa **y** entorno, rotación de refresh-token con detección de reutilización, además de revocación e introspección. * **Gobernado por scopes** — cada tool aplica un scope granular; las tools a las que no puedes acceder quedan ocultas en `tools/list`. Consulta [Scopes y permisos](/mcp/scopes). * **Errores fieles a v1** — los errores JSON-RPC conservan el mismo `code` y `http_status` que la API REST. Consulta [Errores y límites de peticiones](/mcp/errors). * **Claude Code** — el plugin oficial `factuarea-mcp` [plugin](/mcp/claude-code-plugin) conecta en dos comandos. * **Modo de prueba** — ejecuta todo contra el sandbox aislado. Consulta [Modo de prueba](/mcp/connect#test-mode). ## Empieza en modo de prueba [#empieza-en-modo-de-prueba] La regla de oro en las cuatro superficies: **empieza en modo de prueba**. Crea contra una clave `fact_test_` (o un consentimiento OAuth con el entorno Test), luego cambia a `fact_live_` — sin cambios de código. Bienvenido a la era de las integraciones en Factuarea. --- # Resumen del CLI (/es/cli) El CLI oficial **`factuarea`** maneja la [API REST v1](/api-reference/account/public-api.v1.account.show) desde tu terminal. Es **agent-first** — salida JSON estable, exit codes semánticos y descubrimiento en una sola llamada — e inspirado en Stripe: el árbol de comandos completo se genera desde la especificación OpenAPI, así que nunca se desincroniza de la superficie real. La última versión estable es **`v0.1.3`**. <Callout type="info"> ¿Prefieres que un agente de IA maneje Factuarea directamente? El CLI está hecho para eso. Consulta [Agentes y scripting](/cli/agents) para el contrato JSON y los exit codes, y el [servidor MCP](/mcp) para la alternativa basada en tools. </Callout> ## Instalación [#instalación] <Tabs items="['Homebrew', 'npm', 'curl', 'Desde el código']"> <Tab value="Homebrew"> macOS y Linux: ```bash brew install --cask factuarea/tap/factuarea ``` </Tab> <Tab value="npm"> Cualquier plataforma con **Node 20 o superior**: ```bash npm i -g @factuarea/cli # o: npx @factuarea/cli <comando> ``` </Tab> <Tab value="curl"> Instala un binario firmado en `~/.local/bin`: ```bash curl -fsSL https://github.com/factuarea/factuarea-cli/releases/latest/download/install.sh | sh ``` Los binarios están firmados (cosign) y vienen con `checksums.txt` en [Releases](https://github.com/factuarea/factuarea-cli/releases). </Tab> <Tab value="Desde el código"> Requiere **Go 1.26 o superior**: ```bash git clone https://github.com/factuarea/factuarea-cli && cd factuarea-cli make build # genera ./factuarea ``` </Tab> </Tabs> <Callout type="warn"> La notarización en macOS y la firma Authenticode en Windows llegan en una fase posterior. Por ahora, en macOS usa `brew` o `npm`, o ejecuta `xattr -d com.apple.quarantine ./factuarea` sobre un binario suelto. </Callout> ## Autenticación [#autenticación] El CLI usa tu **API key** de Factuarea. El prefijo de la key decide el entorno — no hay un flag aparte: * `fact_test_…` → el [sandbox](/guides/test-mode) aislado: datos de prueba, sin efectos reales (no transmite a la AEAT, no envía email, no entrega webhooks). * `fact_live_…` → producción: datos reales. <Steps> <Step> **Inicia sesión** ```bash factuarea login # te pide la key en un prompt oculto ``` La key se lee en un prompt oculto — nunca se pasa como argumento visible. Se guarda en el keyring del sistema (con fallback a `~/.config/factuarea/config.toml`, permisos 600). Soporta múltiples **perfiles** con `--profile`. </Step> <Step> **O define una variable de entorno** Para entornos no interactivos: ```bash export FACTUAREA_API_KEY=fact_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` </Step> <Step> **Verifica** ```bash factuarea whoami # muestra la cuenta y el entorno (TEST/LIVE) ``` </Step> </Steps> <Callout type="info"> Empieza toda integración con una key **`fact_test_`**. La superficie de comandos es idéntica a producción — cambia el prefijo a `fact_live_` solo cuando tu flujo funcione de principio a fin. Las mutaciones en producción (con una key `fact_live_`) requieren además el flag explícito `--live` como red de seguridad. </Callout> ## Qué sigue [#qué-sigue] <Cards> <Card icon="<Terminal />" title="Uso" href="/cli/usage"> El árbol de comandos generado — list, show, create, acciones de dominio, descargas binarias, el escape hatch `api` y `commands --json`. </Card> <Card icon="<Webhook />" title="Devloop" href="/cli/devloop"> Prueba webhooks en local sin desplegar ni ngrok: `listen` reenvía los eventos a tu máquina, `trigger` produce eventos reales en sandbox. </Card> <Card icon="<Bot />" title="Agentes y scripting" href="/cli/agents"> El contrato agent-first: JSON estable por stdout, errores estructurados por stderr, exit codes semánticos, scope-check y confirmación tipada. </Card> </Cards> --- # Agentes y scripting (/es/cli/agents) El CLI es **agent-first**: un asistente de IA o un script puede descubrir toda la superficie en una llamada, obtener salida estable legible por máquina, y ramificar según exit codes semánticos en lugar de parsear prosa. ## Descubre la superficie en una llamada [#descubre-la-superficie-en-una-llamada] ```bash factuarea commands --json ``` Esto vuelca el manifiesto completo de comandos. Cada entrada lleva: | Campo | Significado | | ---------------- | ----------------------------------------------------------- | | `path` | El path del comando, p. ej. `invoices create`. | | `args` | Argumentos posicionales (path params). | | `flags` | Flags disponibles. | | `mutating` | Si el comando escribe (necesita `--live` en producción). | | `binary` | Si devuelve un binario (PDF/ZIP/XML) en lugar de JSON. | | `paginated` | Si el comando soporta paginación por cursor. | | `required_scope` | El scope que la API key debe tener, p. ej. `invoices:read`. | | `irreversible` | Si la operación no se puede deshacer. | | `example` | Una invocación de ejemplo lista para adaptar. | `required_scope` e `irreversible` vienen directamente de las extensiones `x-required-scope` y `x-irreversible` de la especificación OpenAPI, así que el CLI y la [referencia de la API](/api-reference/account/public-api.v1.account.show) coinciden por construcción. ## Contrato de salida [#contrato-de-salida] * `--json` emite el **cuerpo crudo de la API** por **stdout**. * Los errores van a **stderr** como JSON estructurado — el mismo [envoltorio de error](/guides/errors) que la API: `error.{type,code,message,request_id,doc_url}`. * Reserva stdout para los datos y stderr para los diagnósticos: canaliza stdout a `jq`, registra stderr. ## Exit codes [#exit-codes] Ramifica según el exit code, nunca según el mensaje: | Código | Significado | | ------ | -------------------------- | | `0` | OK | | `2` | Error de uso / guard local | | `3` | Fallo de autenticación | | `4` | Permiso / scope ausente | | `5` | Error de validación | | `6` | No encontrado | | `7` | Límite de peticiones | | `8` | Conflicto / idempotencia | | `9` | Error del servidor | | `10` | Red / timeout | ## Scope-check local [#scope-check] Antes de una llamada, el CLI comprueba que tu key tiene el `required_scope` de la operación. Si no lo tiene, el comando **falla en local con exit `4`** y un mensaje claro — sin gastar un round trip que la API rechazaría con `403` de todos modos. * La comprobación solo corre cuando la operación declara un scope y resuelve los scopes de la key de forma lazy (como mucho un `GET /v1/account` por invocación, memoizado). * Un scope `*` en la key cubre cualquier operación. * `--skip-scope-check` degrada el bloqueo a un aviso y continúa — útil si tus scopes cacheados están desactualizados. El `403` real de la API sigue siendo la última línea de defensa. ## Operaciones irreversibles [#irreversible-operations] Las operaciones que la especificación marca `x-irreversible` (borrados, `void`, conversiones terminales, emisión fiscal, rotación de certificado, olvido GDPR…) piden una confirmación tipada antes de la llamada: ```bash factuarea invoices delete <uuid> --confirm <uuid> ``` * Pasa `--confirm <id>` con el id del recurso para continuar. * En un contexto no interactivo (`--no-input` o sin TTY) sin `--confirm`, el comando se niega con exit `2` en lugar de adivinar. Consulta la [guía de scopes e irreversibilidad](/guides/scopes-and-irreversibility) para la lista completa de qué operaciones llevan cada scope y cuáles son irreversibles. <Callout type="info"> ¿Quieres que el agente maneje Factuarea a través de tools en lugar de comandos del CLI? Conéctalo al [servidor MCP](/mcp) — la misma superficie expuesta como tools, con autenticación OAuth y por API key. </Callout> --- # Devloop (/es/cli/devloop) Prueba tus webhooks en local sin desplegar ni ngrok, al estilo del CLI de Stripe. El bucle tiene dos mitades: **`listen`** reenvía los eventos de tu cuenta a tu máquina, **`trigger`** produce eventos reales en sandbox para reenviar. ## Reenviar eventos a localhost [#reenviar-eventos-a-localhost] ```bash factuarea listen --forward-to http://localhost:3000/webhooks ``` `listen` sondea el feed de eventos, reconstruye el cuerpo del webhook y lo firma con HMAC (`Factuarea-Signature`) usando un secret efímero `whsec_…` que imprime al arrancar. Configura ese secret en tu verificador y tu código de verificación corre sin cambios — sin diferencias de código entre local y producción. <Callout type="warn"> Por seguridad, `listen` solo reenvía a `localhost`. Para reenviar a un host remoto, pasa `--allow-remote-forward` explícitamente. </Callout> ## Producir eventos para probar [#producir-eventos-para-probar] En otra terminal, produce eventos reales en el sandbox: ```bash factuarea trigger invoice.paid factuarea trigger --list # eventos soportados ``` <Callout type="info"> `trigger` solo opera en el **sandbox** — requiere una key `fact_test_`. Nunca produce eventos contra datos de producción. </Callout> ## Por qué la verificación queda idéntica [#por-qué-la-verificación-queda-idéntica] El esquema de firma es el mismo que usa la plataforma, así que el verificador que despliegas a producción es el verificador con el que pruebas en local: * HMAC-SHA256 sobre el cuerpo crudo con una comparación de tiempo constante. * Una tolerancia de timestamp que rechaza los reenvíos. * Ambas firmas aceptadas durante una ventana de gracia de rotación del secret. La única diferencia es el secret: en local es el `whsec_…` efímero de `listen`; en producción es el secret del endpoint. Consulta [Webhooks](/guides/webhooks) para el contrato de firma completo y los verificadores de los SDKs. <Callout type="info"> Una fase futura sustituye el sondeo de `listen` por un relay WebSocket. La superficie de comandos queda igual; solo cambia el transporte. </Callout> --- # Uso (/es/cli/usage) El árbol de comandos cubre todos los recursos de la API (`factuarea <recurso> [<sub-recurso>] <acción>`), generado desde la especificación OpenAPI para que nunca se desincronice de la superficie real. ## Leer datos [#leer-datos] ```bash # Listar (con paginación automática por cursor) factuarea invoices list --json factuarea clients list --paginate --json # Obtener uno factuarea invoices show <uuid> --json ``` `--json` emite el cuerpo crudo de la API por **stdout**. `--paginate` recorre todas las páginas por ti, siguiendo `next_cursor` hasta que `has_more` sea falso. Consulta [Paginación](/guides/pagination) para la semántica del cursor subyacente. ## Escribir datos [#escribir-datos] Pasa el cuerpo JSON con `-d` (en línea) o `--data-file` (una ruta). La API calcula los totales — no los redondees por adelantado. ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","lines":[…]}' ``` Cada mutación recibe un `Idempotency-Key` automático para que una petición reintentada nunca cree el recurso dos veces. Consulta [Idempotencia](/guides/idempotency). ## Acciones de dominio [#acciones-de-dominio] Los cambios de estado son **acciones discretas**, no un flag de estado genérico — reflejando el propio diseño de la API: ```bash factuarea invoices send <uuid> factuarea invoices mark-paid <uuid> ``` <Callout type="warn"> Algunas acciones son **irreversibles** (borrados, `void`, conversiones, emisión fiscal). El CLI te pide confirmarlas antes de la llamada — consulta [Operaciones irreversibles](/cli/agents#irreversible-operations) y la [guía de scopes e irreversibilidad](/guides/scopes-and-irreversibility). </Callout> ## Control horario (fichajes y ausencias) [#control-horario-fichajes-y-ausencias] El add-on de control horario añade los recursos de jornada — empleados, horarios, fichajes, ausencias, presencia, festivos, cierres mensuales y el resumen de gestoría. Cada comando se genera desde la spec y queda protegido por su scope fino (`employees:*`, `time_entries:*`, `absences:*`, `work_schedules:*`, `presence:read`, `holidays:read`, `payroll_exports:*`). ```bash # Fichar entrada y salida (cada asiento encadena su huella — RD-ley 8/2019) factuarea time-entries clock-in -d '{"employee_id":"…","source":"web"}' factuarea time-entries clock-out -d '{"employee_id":"…","source":"web"}' # Solicitar una ausencia y aprobarla factuarea absence-requests create \ -d '{"employee_id":"…","absence_type_id":"…","start_date":"2026-08-01","end_date":"2026-08-05"}' factuarea absence-requests approve <uuid> # Presencia del equipo en vivo factuarea presence live --json # Cerrar el registro mensual inalterable y exportarlo (ITSS RD-ley 8/2019) factuarea monthly-time-record-closes create -d '{"year":2026,"month":7}' factuarea monthly-time-record-closes export <uuid> --format rdley_8_2019 --json ``` ## Descargas binarias y subidas [#descargas-binarias-y-subidas] Los endpoints de PDF, ZIP y XML transmiten un binario que guardas con `-o`. Las subidas multipart toman el archivo con un flag `--file-<campo>`: ```bash # Descargar un PDF factuarea invoices pdf <uuid> -o invoice.pdf # Subir un certificado (multipart) factuarea verifactu certificates upload \ -d '{"certificate_password":"…"}' --file-certificate_file cert.p12 ``` ## El escape hatch `api` [#el-escape-hatch-api] Cualquier endpoint es accesible directamente con `factuarea api <método> <ruta>`, incluso los que aún no tienen un comando dedicado: ```bash factuarea api get /v1/account --json factuarea api post /v1/invoices -d '{…}' ``` ## El manifiesto de comandos [#el-manifiesto-de-comandos] `factuarea commands --json` vuelca el **manifiesto completo** de comandos en una sola llamada — path, args, flags, si cada uno muta, si es binario o paginado, su scope requerido, si es irreversible, y un ejemplo. Un agente descubre toda la superficie en una sola llamada: ```bash factuarea commands --json ``` Consulta [Agentes y scripting](/cli/agents) para los campos del manifiesto y el contrato JSON. ## Referencia de la API embebida [#referencia-de-la-api-embebida] Una referencia rápida de la API viaja con el binario — las búsquedas no salen de tu máquina: ```bash factuarea docs search invoice ``` `docs search` consulta la **especificación OpenAPI embebida en el binario** y responde a «¿qué comando llamo?». Devuelve *operaciones* — comando, resumen, método y ruta — y nunca toca la red. ## Buscar en la documentación publicada [#buscar-en-la-documentación-publicada] `docs list`, `docs grep` y `docs get` consultan la **documentación publicada** —el corpus `llms-full` de [docs.factuarea.com](https://docs.factuarea.com)— y responden a «¿qué dice la documentación sobre esto?». Devuelven *páginas y secciones*, de las guías, la referencia de la API y el catálogo de errores: ```bash factuarea docs list # todas las páginas: <ruta> — <título> factuarea docs list /guides # solo las que cuelgan de ese prefijo factuarea docs grep "idempotency-key" # secciones de documentación que coinciden factuarea docs get /guides/idempotency # la página entera, en Markdown ``` El corpus se descarga **entero y una sola vez**, se guarda en el directorio de caché del sistema (`~/Library/Caches/factuarea/docs/` en macOS, `~/.cache/factuarea/docs/` en Linux) y se filtra en local. Mientras la copia tenga menos de **15 minutos**, no hay ninguna petición de red, así que una sesión que encadene `list`, `grep` y `get` descarga una vez. <Callout type="info"> **Tu término de búsqueda nunca sale de la máquina.** La URL que se pide es fija y no depende de lo que teclees — no hay ningún servidor de búsqueda al otro lado. Ninguno de los cuatro subcomandos de `docs` lee ni envía una API key. </Callout> | Opción | Qué hace | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `--refresh` | Vuelve a descargar, ignorando una copia todavía vigente | | `--lang` | Idioma de las guías: `en`, `es` o `ca` (por defecto `en`, el idioma fuente). La referencia de la API no se traduce y sale siempre | | `--json` | Salida estable por stdout: `path`/`title` en `list`, `path`/`title`/`section`/`snippet` en `grep`, `path`/`title`/`markdown` en `get` | Si la descarga falla y hay una copia en caché —aunque esté caducada—, se usa esa copia, el aviso va a **stderr** para que el JSON de stdout siga siendo parseable, y el exit code es `0`. Sin ninguna copia, el exit code es `10` (red). Apunta `FACTUAREA_DOCS_URL` a otro origen para descargar el corpus desde ahí. --- # Códigos de error por categoría (/es/errors) Cada `code` de error es estable entre versiones y tiene su propia página con la causa y la acción a tomar. Elige una categoría, o abre la tabla de referencia completa. | Categoría | Códigos | | ----------------------------------------------------------- | ------- | | [Albaranes](/es/errors/index-delivery-notes) | 4 | | [Autenticación](/es/errors/index-authentication) | 7 | | [Autorización](/es/errors/index-authorization) | 9 | | [Clientes](/es/errors/index-clients) | 9 | | [Cuenta](/es/errors/index-account) | 3 | | [Empleados](/es/errors/index-employees) | 2 | | [Empresas](/es/errors/index-companies) | 5 | | [Events](/es/errors/index-events) | 1 | | [Facturas](/es/errors/index-invoices) | 40 | | [Facturas de compra](/es/errors/index-purchase-invoices) | 15 | | [Facturas proforma](/es/errors/index-proformas) | 18 | | [Facturas recurrentes](/es/errors/index-recurring-invoices) | 15 | | [Idempotency](/es/errors/index-idempotency) | 3 | | [Impuestos](/es/errors/index-taxes) | 26 | | [Informes fiscales](/es/errors/index-tax-reports) | 6 | | [Límite de tasa](/es/errors/index-rate-limit) | 2 | | [Notificaciones](/es/errors/index-notifications) | 1 | | [Pagos](/es/errors/index-payments) | 5 | | [Presupuestos](/es/errors/index-quotes) | 4 | | [Productos](/es/errors/index-products) | 6 | | [Proveedores](/es/errors/index-suppliers) | 2 | | [Request](/es/errors/index-request) | 38 | | [Series](/es/errors/index-series) | 17 | | [Servidor](/es/errors/index-server) | 9 | | [VeriFactu](/es/errors/index-verifactu) | 25 | | [Webhooks](/es/errors/index-webhooks) | 13 | ## Relacionado [#relacionado] * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # account_not_found (/es/errors/account_not_found) | Code | Type | HTTP | Categoría | | ------------------- | ----------------- | ---- | ---------------------------------- | | `account_not_found` | `not_found_error` | 404 | [Cuenta](/es/errors/index-account) | ## Causa [#causa] No se pudo resolver la cuenta asociada a la clave, lo que suele significar que la clave ya no apunta a una empresa viva. ## Qué hacer [#qué-hacer] Comprueba que la clave pertenece a una empresa activa y vuelve a emitirla si la empresa cambió. ## Relacionado [#relacionado] * [Todos los códigos de error de Cuenta](/es/errors/index-account) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # addon_not_active (/es/errors/addon_not_active) | Code | Type | HTTP | Categoría | | ------------------ | --------------------- | ---- | ---------------------------------------------- | | `addon_not_active` | `authorization_error` | 403 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] La funcionalidad pertenece a un add-on que ahora mismo no está activo para la empresa. ## Qué hacer [#qué-hacer] Contrata o renueva el add-on; a diferencia de un problema de scope, ninguna clave da acceso a una funcionalidad no contratada. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Tu plan actual no incluye acceso a la API pública. Contrata o renueva un plan de Factuarea para usar la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # addon_required (/es/errors/addon_required) | Code | Type | HTTP | Categoría | | ---------------- | ------------------------ | ---- | ------------------------------------- | | `addon_required` | `payment_required_error` | 402 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] Crear endpoints de webhook pertenece al add-on Developer API, y la empresa no lo tiene activo: el nivel gratuito permite cero endpoints. ## Qué hacer [#qué-hacer] Contrata el add-on y repite la llamada; a diferencia de un problema de permisos, aquí lo que falta es la contratación, no el scope. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # alta_record_not_found (/es/errors/alta_record_not_found) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------- | ---- | --------------------------------------- | | `alta_record_not_found` | `not_found_error` | 404 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La factura no tiene registro de alta, así que la operación que depende de él no tiene sobre qué trabajar. ## Qué hacer [#qué-hacer] Comprueba que la factura se emitió con VeriFactu activo; si el registro quedó diferido por un problema de certificado, arregla el certificado y se creará. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # alternative_id_type_invalid (/es/errors/alternative_id_type_invalid) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | ------------------------------------ | | `alternative_id_type_invalid` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] El tipo de identificador alternativo queda fuera del catálogo `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. ## Qué hacer [#qué-hacer] Envía el tipo que corresponde al documento que estás registrando; se declara a la AEAT junto al identificador. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # anulacion_record_already_exists (/es/errors/anulacion_record_already_exists) | Code | Type | HTTP | Categoría | | --------------------------------- | ---------------- | ---- | --------------------------------------- | | `anulacion_record_already_exists` | `conflict_error` | 409 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La factura ya tiene un registro de anulación en la cadena, y la anulación se declara una sola vez. ## Qué hacer [#qué-hacer] Lee el registro existente para comprobar su estado AEAT en vez de volver a anular. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # api_key_already_revoked (/es/errors/api_key_already_revoked) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ---------------------------------- | | `api_key_already_revoked` | `invalid_request_error` | 422 | [Cuenta](/es/errors/index-account) | ## Causa [#causa] La clave ya estaba revocada, y una clave revocada no admite más operaciones: la revocación es terminal. ## Qué hacer [#qué-hacer] Emite una clave nueva si necesitas credenciales otra vez; en esta no queda nada que revocar ni que rotar. ## Relacionado [#relacionado] * [Todos los códigos de error de Cuenta](/es/errors/index-account) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # api_key_expired (/es/errors/api_key_expired) | Code | Type | HTTP | Categoría | | ----------------- | ---------------------- | ---- | ------------------------------------------------ | | `api_key_expired` | `authentication_error` | 401 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] La clave pasó su fecha de caducidad. ## Qué hacer [#qué-hacer] Emite una clave nueva; si usas fechas de caducidad, planifica la rotación antes de la fecha para que la integración no se quede a oscuras. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Esta API key ha caducado. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # api_key_not_found (/es/errors/api_key_not_found) | Code | Type | HTTP | Categoría | | ------------------- | ----------------- | ---- | ---------------------------------- | | `api_key_not_found` | `not_found_error` | 404 | [Cuenta](/es/errors/index-account) | ## Causa [#causa] El identificador no corresponde a ninguna API key de la empresa autenticada. ## Qué hacer [#qué-hacer] Lista tus claves y usa el `id` que devuelven; el secreto de una clave nunca es un identificador válido. ## Relacionado [#relacionado] * [Todos los códigos de error de Cuenta](/es/errors/index-account) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # api_key_revoked (/es/errors/api_key_revoked) | Code | Type | HTTP | Categoría | | ----------------- | ---------------------- | ---- | ------------------------------------------------ | | `api_key_revoked` | `authentication_error` | 401 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] La clave fue revocada, y una clave revocada no vuelve a autenticar nunca: revocar es justamente la forma de cortar una credencial filtrada. ## Qué hacer [#qué-hacer] Emite una clave nueva y despliégala allí donde estuviera la antigua. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Esta API key ha sido revocada. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # api_version_invalid_format (/es/errors/api_version_invalid_format) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------------- | ---- | ------------------------------------- | | `api_version_invalid_format` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] La versión de payload del endpoint no es una fecha `YYYY-MM-DD`. ## Qué hacer [#qué-hacer] Envía la versión como fecha, coincidiendo con una de las versiones de payload publicadas. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # api_version_unsupported (/es/errors/api_version_unsupported) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ------------------------------------- | | `api_version_unsupported` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] La versión de payload está bien formada pero no está entre las que sirve la plataforma. ## Qué hacer [#qué-hacer] Elige una versión soportada, o deja el campo fuera para recibir los eventos en la vigente. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # attachment_invalid_filename (/es/errors/attachment_invalid_filename) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `attachment_invalid_filename` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El nombre del fichero no es utilizable: está vacío, lleva componentes de ruta, o supera los 200 caracteres. ## Qué hacer [#qué-hacer] Envía un nombre de fichero simple con su extensión, sin directorios ni segmentos `../`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # attachment_mime_not_allowed (/es/errors/attachment_mime_not_allowed) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `attachment_mime_not_allowed` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El tipo de fichero queda fuera del conjunto admitido: PDF, PNG, JPEG, XML y HTML. ## Qué hacer [#qué-hacer] Convierte el documento a PDF o envía el original que emitió el proveedor; las hojas de cálculo y los documentos de ofimática no se aceptan como adjunto fiscal. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # attachment_missing (/es/errors/attachment_missing) | Code | Type | HTTP | Categoría | | -------------------- | ----------------- | ---- | -------------------------------------------------------- | | `attachment_missing` | `not_found_error` | 404 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] La factura de compra existe pero no tiene fichero adjunto, así que no hay nada que descargar. ## Qué hacer [#qué-hacer] Sube el documento del proveedor a la factura antes de pedir el fichero. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # attachment_too_large (/es/errors/attachment_too_large) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `attachment_too_large` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El fichero supera el tamaño máximo permitido para un adjunto de documento. ## Qué hacer [#qué-hacer] Comprime el PDF o baja la resolución del escaneo antes de subirlo. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # business_rule_violation (/es/errors/business_rule_violation) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `business_rule_violation` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Una invariante del dominio rechazó la operación. Este código indica la familia; `error.subcode` nombra la regla concreta y `error.message` la explica. ## Qué hacer [#qué-hacer] Busca el `error.subcode` en la referencia de errores: el payload puede ser correcto y la operación seguir sin estar permitida en el estado actual. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # cannot_archive_last_default_series (/es/errors/cannot_archive_last_default_series) | Code | Type | HTTP | Categoría | | ------------------------------------ | ----------------------- | ---- | --------------------------------- | | `cannot_archive_last_default_series` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] La serie es la única activa de su tipo de documento. Archivarla dejaría a la empresa sin numeración disponible y congelaría ese tipo de documento. ## Qué hacer [#qué-hacer] Crea otra serie del mismo tipo, márcala como default y archiva esta después. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # cannot_attach_to_cancelled_purchase_invoice (/es/errors/cannot_attach_to_cancelled_purchase_invoice) | Code | Type | HTTP | Categoría | | --------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `cannot_attach_to_cancelled_purchase_invoice` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] La factura está cancelada, y adjuntar documentos a un registro cancelado alteraría documentación ya cerrada. ## Qué hacer [#qué-hacer] Vuelve a registrar el gasto en una factura viva y adjunta ahí el fichero. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # cannot_have_both_tax_id_and_alternative_id (/es/errors/cannot_have_both_tax_id_and_alternative_id) | Code | Type | HTTP | Categoría | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `cannot_have_both_tax_id_and_alternative_id` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] El cliente envía `tax_id` y un identificador alternativo a la vez. La identidad fiscal es una: el identificador alternativo existe precisamente para partes sin NIF español. ## Qué hacer [#qué-hacer] Deja `tax_id` para partes españolas, o el identificador alternativo con su tipo para las extranjeras, y vacía el otro campo. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # census_requires_tax_id (/es/errors/census_requires_tax_id) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ------------------------------------ | | `census_requires_tax_id` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] La verificación censal contrasta el par nombre + NIF contra la AEAT, y falta uno de los dos. ## Qué hacer [#qué-hacer] Rellena el NIF de la parte que se verifica antes de pedir la comprobación. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # certificate_expired (/es/errors/certificate_expired) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------------- | | `certificate_expired` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El certificado está fuera de su ventana de validez: ha caducado, o todavía no es válido. ## Qué hacer [#qué-hacer] Renueva el certificado en la FNMT y sube el nuevo; al subir uno válido se reencolan los registros que quedaron pendientes. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # certificate_nif_mismatch (/es/errors/certificate_nif_mismatch) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | --------------------------------------- | | `certificate_nif_mismatch` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El NIF del titular del certificado no coincide con el de la empresa. Los registros AEAT se firman en nombre de la empresa, así que ambos deben ser el mismo. ## Qué hacer [#qué-hacer] Sube el certificado emitido para el NIF de esta empresa, o corrige el `tax_id` de la empresa si es ahí donde está el error. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # certificate_not_found (/es/errors/certificate_not_found) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------- | ---- | --------------------------------------- | | `certificate_not_found` | `not_found_error` | 404 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La empresa no tiene ningún certificado FNMT que corresponda al identificador, o no tiene ninguno subido. ## Qué hacer [#qué-hacer] Sube el certificado `.p12` de la empresa; sin él no se puede firmar ni transmitir ningún registro. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # certificate_too_large (/es/errors/certificate_too_large) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | --------------------------------------- | | `certificate_too_large` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El fichero supera el límite de 100 KB, cuando un certificado FNMT real pesa unos pocos kilobytes. ## Qué hacer [#qué-hacer] Asegúrate de subir el certificado en sí y no un paquete, un archivo comprimido o una copia de seguridad que lo contenga. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # client_has_documents (/es/errors/client_has_documents) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ------------------------------------ | | `client_has_documents` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] El cliente está referenciado por documentos emitidos. Borrarlo dejaría facturas, presupuestos o albaranes sin la parte a la que se emitieron, y los registros fiscales tienen que seguir siendo trazables. ## Qué hacer [#qué-hacer] Desactiva el cliente en lugar de borrarlo: deja de aparecer en los selectores y sus documentos conservan la referencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # client_import_too_large (/es/errors/client_import_too_large) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ------------------------------------ | | `client_import_too_large` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] El CSV supera el límite de filas que admite la importación síncrona, ya que el fichero entero se procesa dentro de la propia petición. ## Qué hacer [#qué-hacer] Parte el fichero en lotes más pequeños e impórtalos uno tras otro. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # client_not_found (/es/errors/client_not_found) | Code | Type | HTTP | Categoría | | ------------------ | ----------------- | ---- | ------------------------------------ | | `client_not_found` | `not_found_error` | 404 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] El identificador no resuelve a ningún cliente de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo, o busca el cliente por `tax_id` o por `external_id` antes de crear un duplicado. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # client_requires_tax_identity (/es/errors/client_requires_tax_identity) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ------------------------------------ | | `client_requires_tax_identity` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] El cliente no tiene identidad fiscal: ni `tax_id` ni identificador alternativo, y no se puede emitir una factura a una parte sin identificar. ## Qué hacer [#qué-hacer] Rellena `tax_id`, o un identificador alternativo con su tipo cuando el cliente no tenga NIF español. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # clock_drift_exceeded (/es/errors/clock_drift_exceeded) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | --------------------------------------- | | `clock_drift_exceeded` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El reloj del servidor se desvió del NTP por encima del margen permitido. La marca de tiempo de generación entra en la huella AEAT, así que un reloj desincronizado produciría registros que la AEAT rechaza. ## Qué hacer [#qué-hacer] Es una condición del lado del servidor, no un problema del payload: reintenta en unos minutos y, si persiste, comunica el `request_id` a soporte. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # company_inactive (/es/errors/company_inactive) | Code | Type | HTTP | Categoría | | ------------------ | --------------------- | ---- | -------------------------------------- | | `company_inactive` | `authorization_error` | 403 | [Empresas](/es/errors/index-companies) | ## Causa [#causa] El perfil que indica `X-Active-Profile` es una de tus empresas gestionadas, pero está desactivada y no se puede operar hasta que vuelva a estar activa. ## Qué hacer [#qué-hacer] Reactiva la empresa gestionada, o apunta la cabecera a otro perfil. ## Relacionado [#relacionado] * [Todos los códigos de error de Empresas](/es/errors/index-companies) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # conflicting_pagination_params (/es/errors/conflicting_pagination_params) | Code | Type | HTTP | Categoría | | ------------------------------- | ----------------------- | ---- | ----------------------------------- | | `conflicting_pagination_params` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] `starting_after` y `ending_before` viajaron en la misma petición. Recorren la colección en sentidos opuestos, así que solo puede aplicarse uno. ## Qué hacer [#qué-hacer] Deja un único cursor: `starting_after` para avanzar por la colección, `ending_before` para retroceder. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # corrective_invoice_inanulable (/es/errors/corrective_invoice_inanulable) | Code | Type | HTTP | Categoría | | ------------------------------- | ----------------------- | ---- | ------------------------------------- | | `corrective_invoice_inanulable` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La factura es a su vez una rectificativa, y las rectificativas nunca se anulan: la cadena de corrección tiene que seguir siendo auditable de punta a punta. ## Qué hacer [#qué-hacer] Emite una rectificativa nueva contra la factura original, con los importes correctos. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # custom_header_blocklisted (/es/errors/custom_header_blocklisted) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `custom_header_blocklisted` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] Una de las cabeceras personalizadas está reservada: la gestiona la capa HTTP (`host`, `content-type`, `content-length`, `user-agent`), la envía Factuarea como parte del contrato firmado (`factuarea-*`), o pertenece al proxy (`x-forwarded-*`). ## Qué hacer [#qué-hacer] Renombra la cabecera —`x-mi-app-token` en vez de una reservada— o quítala si la plataforma ya envía esa información. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # custom_header_value_too_long (/es/errors/custom_header_value_too_long) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `custom_header_value_too_long` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El valor de una cabecera personalizada supera los 1024 caracteres. ## Qué hacer [#qué-hacer] Envía un token o una referencia corta en lugar del contenido completo; los datos van en el cuerpo del evento. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # custom_tax_creation_disabled (/es/errors/custom_tax_creation_disabled) | Code | Type | HTTP | Categoría | | ------------------------------ | --------------------- | ---- | ----------------------------------- | | `custom_tax_creation_disabled` | `authorization_error` | 403 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La creación de impuestos personalizados está deshabilitada para esta empresa. ## Qué hacer [#qué-hacer] Usa un impuesto del catálogo canónico y fija tus preferencias mediante los defaults fiscales de la empresa. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # declaracion_already_exists (/es/errors/declaracion_already_exists) | Code | Type | HTTP | Categoría | | ---------------------------- | ---------------- | ---- | --------------------------------------- | | `declaracion_already_exists` | `conflict_error` | 409 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La empresa ya tiene presentada la declaración responsable del SIF de ese período. ## Qué hacer [#qué-hacer] Descarga la declaración existente en lugar de generar una nueva. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # declaracion_not_found (/es/errors/declaracion_not_found) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------- | ---- | --------------------------------------- | | `declaracion_not_found` | `not_found_error` | 404 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La empresa no tiene presentada la declaración responsable del SIF del período solicitado. ## Qué hacer [#qué-hacer] Genera la declaración antes de descargarla o consultarla. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # delivery_note_not_found (/es/errors/delivery_note_not_found) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------- | ---- | -------------------------------------------- | | `delivery_note_not_found` | `not_found_error` | 404 | [Albaranes](/es/errors/index-delivery-notes) | ## Causa [#causa] El identificador no resuelve a ningún albarán de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo, o localiza el albarán por su `external_id`. ## Relacionado [#relacionado] * [Todos los códigos de error de Albaranes](/es/errors/index-delivery-notes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # delivery_note_section_not_editable_in_status (/es/errors/delivery_note_section_not_editable_in_status) | Code | Type | HTTP | Categoría | | ---------------------------------------------- | ----------------------- | ---- | -------------------------------------------- | | `delivery_note_section_not_editable_in_status` | `invalid_request_error` | 422 | [Albaranes](/es/errors/index-delivery-notes) | ## Causa [#causa] La sección logística —transportista, vehículo, conductor— está congelada porque el albarán ya está entregado, facturado o cancelado. ## Qué hacer [#qué-hacer] Registra la corrección en la factura que cobra la entrega, o emite un albarán nuevo si la mercancía vuelve a viajar. ## Relacionado [#relacionado] * [Todos los códigos de error de Albaranes](/es/errors/index-delivery-notes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # dependency_unavailable (/es/errors/dependency_unavailable) | Code | Type | HTTP | Categoría | | ------------------------ | --------------------------- | ---- | ----------------------------------- | | `dependency_unavailable` | `service_unavailable_error` | 503 | [Servidor](/es/errors/index-server) | ## Causa [#causa] Un servicio externo del que depende la operación no respondió a tiempo. ## Qué hacer [#qué-hacer] Reintenta tras una espera breve; si la operación es de escritura, reutiliza la misma `Idempotency-Key` para que el reintento no la duplique. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # direct_debit_requires_default_bank_account (/es/errors/direct_debit_requires_default_bank_account) | Code | Type | HTTP | Categoría | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------ | | `direct_debit_requires_default_bank_account` | `invalid_request_error` | 422 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] Se eligió domiciliación bancaria como método de pago, pero el cliente no tiene cuenta bancaria por defecto a la que cargar. ## Qué hacer [#qué-hacer] Añade una cuenta bancaria al cliente y márcala como predeterminada; después fija el método de pago. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # document_type_required_for_ambiguous_code (/es/errors/document_type_required_for_ambiguous_code) | Code | Type | HTTP | Categoría | | ------------------------------------------- | ----------------------- | ---- | --------------------------------- | | `document_type_required_for_ambiguous_code` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] Ese código de serie existe para más de un tipo de documento, así que por sí solo no identifica una única serie. ## Qué hacer [#qué-hacer] Repite la búsqueda añadiendo el tipo de documento junto al código. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # driver_tax_id_requires_name (/es/errors/driver_tax_id_requires_name) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | -------------------------------------------- | | `driver_tax_id_requires_name` | `invalid_request_error` | 422 | [Albaranes](/es/errors/index-delivery-notes) | ## Causa [#causa] Se envió el NIF del conductor sin su nombre, y un identificador sin nombre no identifica a nadie en el documento de entrega. ## Qué hacer [#qué-hacer] Envía `driver_name` junto a `driver_tax_id`, o deja los dos fuera. ## Relacionado [#relacionado] * [Todos los códigos de error de Albaranes](/es/errors/index-delivery-notes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # duplicate_tax_default_for_document_type (/es/errors/duplicate_tax_default_for_document_type) | Code | Type | HTTP | Categoría | | ----------------------------------------- | ----------------------- | ---- | ----------------------------------- | | `duplicate_tax_default_for_document_type` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] Ya hay otro impuesto del mismo tipo marcado como default para ese tipo de documento, y el par (tipo de impuesto, tipo de documento) admite un único default. ## Qué hacer [#qué-hacer] Quita el default al impuesto que lo ocupa, o marca el nuevo default sobre otro tipo de documento. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # employee_seat_charge_failed (/es/errors/employee_seat_charge_failed) | Code | Type | HTTP | Categoría | | ----------------------------- | ------------------------ | ---- | --------------------------------------- | | `employee_seat_charge_failed` | `payment_required_error` | 402 | [Empleados](/es/errors/index-employees) | ## Causa [#causa] El cobro inmediato del prorrateo del asiento de empleado fue rechazado: la tarjeta se denegó, necesita autenticación, o el proveedor de pago estaba inaccesible. El empleado no se activa si el asiento no se cobra. ## Qué hacer [#qué-hacer] Arregla el método de pago en el portal de facturación y reintenta; consulta con tu banco si la tarjeta se sigue denegando. ## Relacionado [#relacionado] * [Todos los códigos de error de Empleados](/es/errors/index-employees) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # employee_seat_payment_method_required (/es/errors/employee_seat_payment_method_required) | Code | Type | HTTP | Categoría | | --------------------------------------- | ------------------------ | ---- | --------------------------------------- | | `employee_seat_payment_method_required` | `payment_required_error` | 402 | [Empleados](/es/errors/index-employees) | ## Causa [#causa] Dar de alta o reactivar un empleado cobra un asiento de inmediato, y la empresa opera en modo real sin método de pago configurado. ## Qué hacer [#qué-hacer] Abre el portal de facturación en `error.details.payment_setup_url`, registra un método de pago y repite la misma llamada. ## Relacionado [#relacionado] * [Todos los códigos de error de Empleados](/es/errors/index-employees) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # event_already_processed (/es/errors/event_already_processed) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | --------------------------------------- | | `event_already_processed` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] Ese evento del SIF ya está registrado en la cadena de eventos, y cada evento se procesa exactamente una vez. ## Qué hacer [#qué-hacer] No vuelvas a enviar el evento; la entrada existente ya lo cubre. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # event_not_found (/es/errors/event_not_found) | Code | Type | HTTP | Categoría | | ----------------- | ----------------- | ---- | --------------------------------- | | `event_not_found` | `not_found_error` | 404 | [Events](/es/errors/index-events) | ## Causa [#causa] El identificador no corresponde a ningún evento de la empresa autenticada, o el evento fue purgado por la política de retención de 30 días. ## Qué hacer [#qué-hacer] Lee el estado actual desde el recurso al que se refería el evento; el feed de eventos es una ventana reciente, no un archivo permanente. ## Relacionado [#relacionado] * [Todos los códigos de error de Events](/es/errors/index-events) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # export_limit_exceeded (/es/errors/export_limit_exceeded) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ------------------------------------- | | `export_limit_exceeded` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La selección filtrada supera el tope de 5.000 facturas de la exportación, así que el fichero se rechaza de entrada en lugar de truncarse en silencio. ## Qué hacer [#qué-hacer] Acota los filtros — por rango de fechas o por serie — y exporta las facturas en varios lotes. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # external_id_already_exists (/es/errors/external_id_already_exists) | Code | Type | HTTP | Categoría | | ---------------------------- | ---------------- | ---- | ----------------------------------- | | `external_id_already_exists` | `conflict_error` | 409 | [Request](/es/errors/index-request) | ## Causa [#causa] El `external_id` con el que concilias contra tu sistema ya está asignado a otro objeto del mismo tipo en esta empresa. ## Qué hacer [#qué-hacer] Localiza el objeto por su `external_id` y actualízalo, o asigna otro valor: `external_id` es único por tipo de recurso y empresa. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # face_transmission_failed (/es/errors/face_transmission_failed) | Code | Type | HTTP | Categoría | | -------------------------- | ----------- | ---- | ----------------------------------- | | `face_transmission_failed` | `api_error` | 502 | [Servidor](/es/errors/index-server) | ## Causa [#causa] La plataforma FACe —el punto de entrada de las administraciones públicas— estaba inaccesible o respondió con un fallo. El problema está aguas arriba, no en tu petición. ## Qué hacer [#qué-hacer] Reintenta más tarde; la factura conserva su estado y se puede volver a presentar sin reemitirla. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # facturae_signing_failed (/es/errors/facturae_signing_failed) | Code | Type | HTTP | Categoría | | ------------------------- | ----------- | ---- | ----------------------------------- | | `facturae_signing_failed` | `api_error` | 500 | [Servidor](/es/errors/index-server) | ## Causa [#causa] No se pudo producir la firma XAdES del fichero Facturae, normalmente porque el certificado de firma no es utilizable en ese momento. ## Qué hacer [#qué-hacer] Comprueba que el certificado de la empresa es válido y coincide con su NIF; una vez arreglado, vuelve a generar el fichero. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # feature_not_available_in_plan (/es/errors/feature_not_available_in_plan) | Code | Type | HTTP | Categoría | | ------------------------------- | --------------------- | ---- | ---------------------------------------------- | | `feature_not_available_in_plan` | `authorization_error` | 403 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] La funcionalidad no está incluida en el plan de la empresa. ## Qué hacer [#qué-hacer] Sube a un plan que la incluya, o usa la funcionalidad equivalente que sí ofrece tu plan actual. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # forbidden_action (/es/errors/forbidden_action) | Code | Type | HTTP | Categoría | | ------------------ | --------------------- | ---- | ---------------------------------------------- | | `forbidden_action` | `authorization_error` | 403 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] La acción está bloqueada para este recurso aunque el scope sea el correcto: el recurso pertenece a un catálogo compartido, o el cambio va por otro endpoint. ## Qué hacer [#qué-hacer] Lee `error.subcode` y `error.message`: indican la vía canónica para lo que estás intentando hacer. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # gestoria_module_required (/es/errors/gestoria_module_required) | Code | Type | HTTP | Categoría | | -------------------------- | --------------------- | ---- | -------------------------------------- | | `gestoria_module_required` | `authorization_error` | 403 | [Empresas](/es/errors/index-companies) | ## Causa [#causa] La gestoría tiene un plan vigente, pero sin el módulo de gestoría, así que no puede crear ni operar empresas gestionadas. ## Qué hacer [#qué-hacer] Sube a un plan que incluya el módulo; esto es un límite de plan, no un pago pendiente. ## Relacionado [#relacionado] * [Todos los códigos de error de Empresas](/es/errors/index-companies) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # gestoria_plan_required (/es/errors/gestoria_plan_required) | Code | Type | HTTP | Categoría | | ------------------------ | ------------------------ | ---- | -------------------------------------- | | `gestoria_plan_required` | `payment_required_error` | 402 | [Empresas](/es/errors/index-companies) | ## Causa [#causa] La gestoría no tiene una suscripción de pago activa, así que no hay suscripción sobre la que cobrar el asiento. ## Qué hacer [#qué-hacer] Contrata un plan, o reanuda el que canceló, antes de añadir empresas gestionadas. ## Relacionado [#relacionado] * [Todos los códigos de error de Empresas](/es/errors/index-companies) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # idempotency_key_in_use (/es/errors/idempotency_key_in_use) | Code | Type | HTTP | Categoría | | ------------------------ | ------------------- | ---- | ------------------------------------------- | | `idempotency_key_in_use` | `idempotency_error` | 409 | [Idempotency](/es/errors/index-idempotency) | ## Causa [#causa] Hay otra petición con la misma `Idempotency-Key` todavía en curso, y aún no se conoce su resultado. ## Qué hacer [#qué-hacer] Espera a que responda la primera petición y lee su respuesta; reintenta con la misma clave tras una espera breve si se cortó la conexión. ## Relacionado [#relacionado] * [Todos los códigos de error de Idempotency](/es/errors/index-idempotency) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # idempotency_key_invalid (/es/errors/idempotency_key_invalid) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ------------------------------------------- | | `idempotency_key_invalid` | `invalid_request_error` | 400 | [Idempotency](/es/errors/index-idempotency) | ## Causa [#causa] La `Idempotency-Key` no encaja con el formato admitido: entre 1 y 255 caracteres ASCII imprimibles. ## Qué hacer [#qué-hacer] Genera la clave como un UUID o una cadena aleatoria, y mantenla estable entre los reintentos de una misma operación. ## Relacionado [#relacionado] * [Todos los códigos de error de Idempotency](/es/errors/index-idempotency) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # idempotency_key_reused (/es/errors/idempotency_key_reused) | Code | Type | HTTP | Categoría | | ------------------------ | ------------------- | ---- | ------------------------------------------- | | `idempotency_key_reused` | `idempotency_error` | 409 | [Idempotency](/es/errors/index-idempotency) | ## Causa [#causa] Esa `Idempotency-Key` ya se usó con un payload distinto. La clave identifica una operación concreta, así que reutilizarla para otra vaciaría de sentido el replay. ## Qué hacer [#qué-hacer] Usa una clave nueva por cada operación distinta, y reutiliza una clave solo para reintentar exactamente la misma petición. ## Relacionado [#relacionado] * [Todos los códigos de error de Idempotency](/es/errors/index-idempotency) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Cuenta (/es/errors/index-account) Códigos de error que emite Cuenta. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------ | | [`account_not_found`](/es/errors/account_not_found) | `not_found_error` | 404 | No se pudo resolver la cuenta asociada a la clave, lo que suele significar que la clave ya no apunta a una empresa viva. | | [`api_key_already_revoked`](/es/errors/api_key_already_revoked) | `invalid_request_error` | 422 | La clave ya estaba revocada, y una clave revocada no admite más operaciones: la revocación es terminal. | | [`api_key_not_found`](/es/errors/api_key_not_found) | `not_found_error` | 404 | El identificador no corresponde a ninguna API key de la empresa autenticada. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Autenticación (/es/errors/index-authentication) Códigos de error que emite Autenticación. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ---------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`api_key_expired`](/es/errors/api_key_expired) | `authentication_error` | 401 | La clave pasó su fecha de caducidad. | | [`api_key_revoked`](/es/errors/api_key_revoked) | `authentication_error` | 401 | La clave fue revocada, y una clave revocada no vuelve a autenticar nunca: revocar es justamente la forma de cortar una credencial filtrada. | | [`invalid_api_key`](/es/errors/invalid_api_key) | `authentication_error` | 401 | La clave no corresponde a ninguna clave activa. Puede estar mal copiada, truncada, o pertenecer a otro entorno: las claves de prueba y las de producción no son intercambiables. | | [`ip_not_allowed`](/es/errors/ip_not_allowed) | `authentication_error` | 401 | La clave restringe las direcciones que acepta, y la petición llegó desde una que no está en esa lista. | | [`missing_api_key`](/es/errors/missing_api_key) | `authentication_error` | 401 | La petición no lleva credenciales: ni cabecera `Authorization` ni `X-API-Key`. | | [`origin_not_allowed`](/es/errors/origin_not_allowed) | `authentication_error` | 401 | La petición viene de un origen de navegador que la clave no acepta. | | [`too_many_auth_failures`](/es/errors/too_many_auth_failures) | `authentication_error` | 429 | Llegaron demasiados intentos fallidos de autenticación desde la misma dirección, así que queda bloqueada temporalmente para frenar los intentos de adivinar credenciales. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Autorización (/es/errors/index-authorization) Códigos de error que emite Autorización. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------- | --------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_not_active`](/es/errors/addon_not_active) | `authorization_error` | 403 | La funcionalidad pertenece a un add-on que ahora mismo no está activo para la empresa. | | [`feature_not_available_in_plan`](/es/errors/feature_not_available_in_plan) | `authorization_error` | 403 | La funcionalidad no está incluida en el plan de la empresa. | | [`forbidden_action`](/es/errors/forbidden_action) | `authorization_error` | 403 | La acción está bloqueada para este recurso aunque el scope sea el correcto: el recurso pertenece a un catálogo compartido, o el cambio va por otro endpoint. | | [`insufficient_scope`](/es/errors/insufficient_scope) | `authorization_error` | 403 | La clave autentica correctamente pero no lleva el scope que exige esta operación. Los scopes se conceden al emitir la clave y no se amplían en tiempo de llamada. | | [`max_api_keys_exceeded`](/es/errors/max_api_keys_exceeded) | `authorization_error` | 422 | La empresa alcanzó el número de API keys que permite su plan. | | [`max_webhook_endpoints_exceeded`](/es/errors/max_webhook_endpoints_exceeded) | `authorization_error` | 422 | La empresa alcanzó el número de endpoints de webhook que permite su nivel de add-on. | | [`module_not_available_in_sandbox`](/es/errors/module_not_available_in_sandbox) | `authorization_error` | 403 | El recurso pertenece a un módulo vetado en modo test. La sandbox nunca toca AEAT, bancos ni cobros reales, así que esos módulos quedan fuera a propósito. | | [`scope_not_allowed_by_plan`](/es/errors/scope_not_allowed_by_plan) | `authorization_error` | 422 | Uno de los scopes pedidos pertenece a un módulo que el plan no incluye, así que la clave nacería con un permiso que nunca podría ejercer. | | [`scope_not_allowed_in_sandbox`](/es/errors/scope_not_allowed_in_sandbox) | `authorization_error` | 422 | Una clave de prueba no puede nacer con scopes de módulos vetados en sandbox. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Clientes (/es/errors/index-clients) Códigos de error que emite Clientes. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alternative_id_type_invalid`](/es/errors/alternative_id_type_invalid) | `invalid_request_error` | 422 | El tipo de identificador alternativo queda fuera del catálogo `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. | | [`cannot_have_both_tax_id_and_alternative_id`](/es/errors/cannot_have_both_tax_id_and_alternative_id) | `invalid_request_error` | 422 | El cliente envía `tax_id` y un identificador alternativo a la vez. La identidad fiscal es una: el identificador alternativo existe precisamente para partes sin NIF español. | | [`census_requires_tax_id`](/es/errors/census_requires_tax_id) | `invalid_request_error` | 422 | La verificación censal contrasta el par nombre + NIF contra la AEAT, y falta uno de los dos. | | [`client_has_documents`](/es/errors/client_has_documents) | `invalid_request_error` | 422 | El cliente está referenciado por documentos emitidos. Borrarlo dejaría facturas, presupuestos o albaranes sin la parte a la que se emitieron, y los registros fiscales tienen que seguir siendo trazables. | | [`client_import_too_large`](/es/errors/client_import_too_large) | `invalid_request_error` | 422 | El CSV supera el límite de filas que admite la importación síncrona, ya que el fichero entero se procesa dentro de la propia petición. | | [`client_not_found`](/es/errors/client_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún cliente de la empresa autenticada. | | [`client_requires_tax_identity`](/es/errors/client_requires_tax_identity) | `invalid_request_error` | 422 | El cliente no tiene identidad fiscal: ni `tax_id` ni identificador alternativo, y no se puede emitir una factura a una parte sin identificar. | | [`direct_debit_requires_default_bank_account`](/es/errors/direct_debit_requires_default_bank_account) | `invalid_request_error` | 422 | Se eligió domiciliación bancaria como método de pago, pero el cliente no tiene cuenta bancaria por defecto a la que cargar. | | [`tax_id_already_exists`](/es/errors/tax_id_already_exists) | `conflict_error` | 409 | Otro cliente de la empresa ya tiene ese NIF, y el NIF identifica a la parte sin ambigüedad dentro de una empresa. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Empresas (/es/errors/index-companies) Códigos de error que emite Empresas. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------- | ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`company_inactive`](/es/errors/company_inactive) | `authorization_error` | 403 | El perfil que indica `X-Active-Profile` es una de tus empresas gestionadas, pero está desactivada y no se puede operar hasta que vuelva a estar activa. | | [`gestoria_module_required`](/es/errors/gestoria_module_required) | `authorization_error` | 403 | La gestoría tiene un plan vigente, pero sin el módulo de gestoría, así que no puede crear ni operar empresas gestionadas. | | [`gestoria_plan_required`](/es/errors/gestoria_plan_required) | `payment_required_error` | 402 | La gestoría no tiene una suscripción de pago activa, así que no hay suscripción sobre la que cobrar el asiento. | | [`payment_method_required`](/es/errors/payment_method_required) | `payment_required_error` | 402 | Dar de alta una empresa gestionada cobra un asiento de inmediato, y la gestoría opera en modo real sin método de pago configurado. | | [`seat_charge_failed`](/es/errors/seat_charge_failed) | `payment_required_error` | 402 | El cobro inmediato del prorrateo del asiento fue rechazado: la tarjeta se denegó, necesita autenticación, o el proveedor de pago estaba inaccesible. La empresa no se crea si el asiento no se cobra. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Albaranes (/es/errors/index-delivery-notes) Códigos de error que emite Albaranes. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------ | | [`delivery_note_not_found`](/es/errors/delivery_note_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún albarán de la empresa autenticada. | | [`delivery_note_section_not_editable_in_status`](/es/errors/delivery_note_section_not_editable_in_status) | `invalid_request_error` | 422 | La sección logística —transportista, vehículo, conductor— está congelada porque el albarán ya está entregado, facturado o cancelado. | | [`driver_tax_id_requires_name`](/es/errors/driver_tax_id_requires_name) | `invalid_request_error` | 422 | Se envió el NIF del conductor sin su nombre, y un identificador sin nombre no identifica a nadie en el documento de entrega. | | [`signature_payload_too_large`](/es/errors/signature_payload_too_large) | `invalid_request_error` | 422 | La imagen de la firma supera el tamaño admitido para el campo. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Empleados (/es/errors/index-employees) Códigos de error que emite Empleados. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------------------- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`employee_seat_charge_failed`](/es/errors/employee_seat_charge_failed) | `payment_required_error` | 402 | El cobro inmediato del prorrateo del asiento de empleado fue rechazado: la tarjeta se denegó, necesita autenticación, o el proveedor de pago estaba inaccesible. El empleado no se activa si el asiento no se cobra. | | [`employee_seat_payment_method_required`](/es/errors/employee_seat_payment_method_required) | `payment_required_error` | 402 | Dar de alta o reactivar un empleado cobra un asiento de inmediato, y la empresa opera en modo real sin método de pago configurado. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Events (/es/errors/index-events) Códigos de error que emite Events. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ----------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [`event_not_found`](/es/errors/event_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún evento de la empresa autenticada, o el evento fue purgado por la política de retención de 30 días. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Idempotency (/es/errors/index-idempotency) Códigos de error que emite Idempotency. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`idempotency_key_in_use`](/es/errors/idempotency_key_in_use) | `idempotency_error` | 409 | Hay otra petición con la misma `Idempotency-Key` todavía en curso, y aún no se conoce su resultado. | | [`idempotency_key_invalid`](/es/errors/idempotency_key_invalid) | `invalid_request_error` | 400 | La `Idempotency-Key` no encaja con el formato admitido: entre 1 y 255 caracteres ASCII imprimibles. | | [`idempotency_key_reused`](/es/errors/idempotency_key_reused) | `idempotency_error` | 409 | Esa `Idempotency-Key` ya se usó con un payload distinto. La clave identifica una operación concreta, así que reutilizarla para otra vaciaría de sentido el replay. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Facturas (/es/errors/index-invoices) Códigos de error que emite Facturas. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`corrective_invoice_inanulable`](/es/errors/corrective_invoice_inanulable) | `invalid_request_error` | 422 | La factura es a su vez una rectificativa, y las rectificativas nunca se anulan: la cadena de corrección tiene que seguir siendo auditable de punta a punta. | | [`export_limit_exceeded`](/es/errors/export_limit_exceeded) | `invalid_request_error` | 422 | La selección filtrada supera el tope de 5.000 facturas de la exportación, así que el fichero se rechaza de entrada en lugar de truncarse en silencio. | | [`invalid_correction_nature`](/es/errors/invalid_correction_nature) | `invalid_request_error` | 422 | `correction_nature` solo acepta `S` (sustitución: la rectificativa lleva los importes corregidos completos) o `I` (por diferencias: lleva solo el delta). | | [`invalid_correction_reason`](/es/errors/invalid_correction_reason) | `invalid_request_error` | 422 | El motivo de rectificación queda fuera de la lista fiscal cerrada (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), que mapea a los códigos AEAT R1 a R4. | | [`invalid_invoice_id`](/es/errors/invalid_invoice_id) | `invalid_request_error` | 400 | La referencia de factura recibida no es un identificador válido; suele significar que se coló un valor interno donde la API espera el `id` público. | | [`invalid_invoice_number`](/es/errors/invalid_invoice_number) | `invalid_request_error` | 422 | El número de factura no sigue el formato canónico `SERIE-AAAA-NNN`, más el sufijo `-RECn` en las rectificativas. | | [`invalid_invoice_status`](/es/errors/invalid_invoice_status) | `invalid_request_error` | 422 | El valor enviado como estado de factura queda fuera del catálogo del ciclo de vida (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). | | [`invalid_invoice_uuid`](/es/errors/invalid_invoice_uuid) | `invalid_request_error` | 400 | El identificador de factura de la ruta o del payload no es un UUID válido. | | [`invalid_payment_method`](/es/errors/invalid_payment_method) | `invalid_request_error` | 422 | El método de pago queda fuera de la allowlist cerrada: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. | | [`invoice_already_annulled`](/es/errors/invoice_already_annulled) | `invalid_request_error` | 422 | La factura ya estaba anulada. La anulación es terminal y, con VeriFactu activo, su registro de anulación ya llegó a la AEAT. | | [`invoice_already_paid`](/es/errors/invoice_already_paid) | `invalid_request_error` | 422 | La factura ya está cobrada. `paid` es un estado terminal y contablemente cerrado: el IVA repercutido ya se ha declarado, o se declarará en el período. | | [`invoice_already_sent`](/es/errors/invoice_already_sent) | `invalid_request_error` | 422 | La factura ya fue emitida: tiene número definitivo de serie y, con VeriFactu activo, su alta en la AEAT. La emisión no ocurre dos veces. | | [`invoice_cannot_assign_number`](/es/errors/invoice_cannot_assign_number) | `invalid_request_error` | 422 | Se pidió número definitivo para una factura que no es borrador, o que ya lo tiene. La numeración de serie es monótona y los números no se reasignan. | | [`invoice_invalid_status_transition`](/es/errors/invoice_invalid_status_transition) | `invalid_request_error` | 422 | El estado destino no es alcanzable desde el actual. El ciclo de vida es dirigido: `draft` pasa a `scheduled` o `sent`, `sent` a `paid`, `overdue` o `annulled`, y `paid`, `cancelled` y `annulled` son terminales. | | [`invoice_not_cancellable_in_current_state`](/es/errors/invoice_not_cancellable_in_current_state) | `invalid_request_error` | 422 | Cancelar retira un borrador que todavía no es fiscalmente vinculante, así que solo aplica mientras la factura está en `draft`. | | [`invoice_not_correctable_in_current_state`](/es/errors/invoice_not_correctable_in_current_state) | `invalid_request_error` | 422 | Una rectificativa solo se emite contra una factura ya emitida (`sent` o `paid`). Un borrador, una factura cancelada o una anulada no tienen nada que rectificar. | | [`invoice_not_deletable_in_current_state`](/es/errors/invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Solo se borran las facturas en `draft` y `cancelled`. Una factura numerada nunca desaparece: la serie correlativa debe seguir siendo auditable. | | [`invoice_not_editable_in_current_state`](/es/errors/invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Solo un borrador admite edición. Una vez emitida, la factura es inmutable y su contenido queda congelado junto con su registro fiscal. | | [`invoice_not_eligible_for_action`](/es/errors/invoice_not_eligible_for_action) | `invalid_request_error` | 422 | La acción solicitada no aplica a esta factura: su tipo o su estado actual la dejan fuera del alcance de la operación. | | [`invoice_not_found`](/es/errors/invoice_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna factura de la empresa autenticada. Las facturas de otra empresa responden exactamente igual. | | [`invoice_not_modifiable_in_current_state`](/es/errors/invoice_not_modifiable_in_current_state) | `invalid_request_error` | 422 | El campo que intentas cambiar está congelado para el estado actual — por ejemplo el régimen fiscal de una factura anulada. | | [`invoice_not_paid`](/es/errors/invoice_not_paid) | `invalid_request_error` | 422 | Se pidió un justificante de pago de una factura sin cobro registrado, así que no hay nada que certificar. | | [`invoice_not_reschedulable_in_current_state`](/es/errors/invoice_not_reschedulable_in_current_state) | `invalid_request_error` | 422 | Reprogramar mueve la fecha de emisión de una factura que está esperando en `scheduled`, y esta factura no está esperando. | | [`invoice_not_schedulable_in_current_state`](/es/errors/invoice_not_schedulable_in_current_state) | `invalid_request_error` | 422 | Solo un borrador se puede programar: la programación reserva un momento futuro de emisión sin consumir todavía número de serie. | | [`invoice_not_unschedulable_in_current_state`](/es/errors/invoice_not_unschedulable_in_current_state) | `invalid_request_error` | 422 | Desprogramar devuelve la factura de `scheduled` a `draft`, así que solo aplica mientras sigue esperando a emitirse. | | [`invoice_not_unsendable_in_current_state`](/es/errors/invoice_not_unsendable_in_current_state) | `invalid_request_error` | 422 | Deshacer la marca de entrega solo aplica a una factura `sent`: limpia `sent_at` y mantiene la factura emitida. | | [`invoice_requires_at_least_one_line`](/es/errors/invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura no lleva ninguna línea de operación, así que no tiene base imponible y no se puede emitir. Ocurre cuando no envías líneas y cuando todas las que envías son de suplido: un suplido es una cantidad pagada por cuenta del cliente (art. 78.Tres.3 LIVA), no una operación tuya. | | [`invoice_year_required_for_ambiguous_number`](/es/errors/invoice_year_required_for_ambiguous_number) | `invalid_request_error` | 422 | Ese número de factura existe en más de un ejercicio, así que por sí solo no identifica una única factura. | | [`line_total_checksum_mismatch`](/es/errors/line_total_checksum_mismatch) | `invalid_request_error` | 422 | El `line_total` declarado no coincide con el que calcula Factuarea para esa línea (cantidad × precio − descuento + IVA − retención + recargo) y la desviación supera el céntimo de tolerancia. El importe que se factura y se declara a la AEAT es siempre el calculado aquí, así que la discrepancia significa que tu sistema y la factura emitida no cuadrarían. | | [`line_type_invalid`](/es/errors/line_type_invalid) | `invalid_request_error` | 422 | El tipo de línea queda fuera del catálogo cerrado `NORMAL` / `SUPLIDO`. Una factura emitida sólo distingue dos naturalezas: lo que vendes tú, que forma base imponible y lleva IVA, y el suplido, que es dinero adelantado en nombre y por cuenta del cliente y por eso queda fuera de la base (art. 78.Tres.3 LIVA). | | [`no_invoices_in_period`](/es/errors/no_invoices_in_period) | `invalid_request_error` | 422 | La operación trimestral no encontró facturas en el período pedido, así que no hay nada que empaquetar ni enviar. | | [`payment_method_invalid`](/es/errors/payment_method_invalid) | `invalid_request_error` | 422 | La misma allowlist cerrada que `invalid_payment_method`, reportada cuando el valor se rechaza al leer el campo de método de pago del payload. | | [`reminder_not_applicable`](/es/errors/reminder_not_applicable) | `invalid_request_error` | 422 | El recordatorio de pago no procede: la factura no está en `sent` ni `overdue`, no hay email de destinatario, falta el enlace público o está desactivado, o ya salió otro recordatorio en las últimas 24 horas. | | [`scheduled_for_in_past`](/es/errors/scheduled_for_in_past) | `invalid_request_error` | 422 | `scheduled_for` no es estrictamente futuro, así que no hay ninguna espera que reservar. | | [`simplified_invoice_cannot_be_substituted`](/es/errors/simplified_invoice_cannot_be_substituted) | `invalid_request_error` | 422 | Una de las facturas de la lista de sustitución no se puede sustituir: no es simplificada, está cancelada o anulada, pertenece a otra empresa, o ya tiene sustitutiva. | | [`simplified_invoice_not_allowed`](/es/errors/simplified_invoice_not_allowed) | `invalid_request_error` | 422 | La operación no es elegible para factura simplificada: supera los 3.000 €, o es una entrega intracomunitaria, una exportación, una operación con inversión del sujeto pasivo, o el cliente necesita factura completa para deducir el IVA. | | [`simplified_limit_exceeded`](/es/errors/simplified_limit_exceeded) | `invalid_request_error` | 422 | Las líneas llevarían la factura simplificada (F2) por encima del tope legal absoluto de 3.000 € IVA incluido. | | [`suplido_line_cannot_carry_taxes`](/es/errors/suplido_line_cannot_carry_taxes) | `invalid_request_error` | 422 | La línea de suplido lleva carga propia: tipo de IVA, retención, recargo de equivalencia, descuento, clave de régimen, causa de exención o producto/pack. Un suplido no es una operación del emisor, así que repercutir un impuesto sobre él sería tributar por una entrega que no has hecho, y ligarlo a un producto movería un stock que nunca has vendido. | | [`suplido_not_allowed_in_simplified_invoice`](/es/errors/suplido_not_allowed_in_simplified_invoice) | `invalid_request_error` | 422 | La factura es simplificada (F2) y una simplificada no identifica al destinatario. Sin destinatario identificado no hay a quién acreditar el pago por cuenta ajena, así que el importe no admite el tratamiento de suplido en este tipo de factura. | | [`suplido_requires_source_invoice_reference`](/es/errors/suplido_requires_source_invoice_reference) | `invalid_request_error` | 422 | La línea de suplido no informa `source_invoice_reference`, el número del justificante que el tercero expidió a nombre del cliente. Sin ese justificante el pago no se acredita como hecho por cuenta ajena y Hacienda lo trataría como base imponible propia del emisor, con su IVA repercutido. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Notificaciones (/es/errors/index-notifications) Códigos de error que emite Notificaciones. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [`notification_not_found`](/es/errors/notification_not_found) | `not_found_error` | 404 | El identificador no corresponde a ninguna notificación de la empresa autenticada, o la notificación quedó fuera de la ventana de retención. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Pagos (/es/errors/index-payments) Códigos de error que emite Pagos. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_payment_date`](/es/errors/invalid_payment_date) | `invalid_request_error` | 422 | La fecha de pago queda fuera de la ventana admitida: no puede ser anterior a la fecha de emisión de la factura ni situarse en el futuro. | | [`payout_reconciliation_amount_mismatch`](/es/errors/payout_reconciliation_amount_mismatch) | `invalid_request_error` | 422 | El importe confirmado no coincide con el neto de la liquidación, así que la conciliación cerraría con una diferencia que nadie justifica. | | [`receipt_not_available`](/es/errors/receipt_not_available) | `invalid_request_error` | 422 | No hay justificante que emitir porque el documento no tiene ningún cobro registrado detrás. | | [`stripe_payout_already_reconciled`](/es/errors/stripe_payout_already_reconciled) | `invalid_request_error` | 422 | La liquidación ya estaba conciliada, y la conciliación es terminal: repetirla contabilizaría dos veces el apunte bancario. | | [`stripe_payout_not_found`](/es/errors/stripe_payout_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna liquidación de la empresa autenticada. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Productos (/es/errors/index-products) Códigos de error que emite Productos. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------- | | [`pack_in_use`](/es/errors/pack_in_use) | `invalid_request_error` | 422 | El pack está referenciado por documentos emitidos, así que borrarlo rompería su composición. | | [`pack_not_found`](/es/errors/pack_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún pack de la empresa autenticada. | | [`pack_share_link_failed`](/es/errors/pack_share_link_failed) | `api_error` | 500 | No se pudo generar el enlace para compartir el pack. El pack en sí no queda afectado. | | [`product_in_use`](/es/errors/product_in_use) | `invalid_request_error` | 422 | El producto está referenciado por documentos emitidos o por otras entradas del catálogo, y eliminarlo dejaría esas referencias colgando. | | [`product_not_found`](/es/errors/product_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún producto de la empresa autenticada. | | [`sku_already_exists`](/es/errors/sku_already_exists) | `conflict_error` | 409 | Otro producto de la empresa ya usa ese SKU, y el SKU identifica al artículo sin ambigüedad dentro del catálogo. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Facturas proforma (/es/errors/index-proformas) Códigos de error que emite Facturas proforma. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`invalid_expiry_date`](/es/errors/invalid_expiry_date) | `invalid_request_error` | 422 | La fecha de vencimiento es anterior a la de emisión, o la supera en más de 365 días. | | [`invalid_proforma_id`](/es/errors/invalid_proforma_id) | `invalid_request_error` | 400 | La referencia de proforma recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. | | [`invalid_proforma_number`](/es/errors/invalid_proforma_number) | `invalid_request_error` | 422 | El número de proforma no sigue el formato canónico de numeración de su serie. | | [`invalid_proforma_status`](/es/errors/invalid_proforma_status) | `invalid_request_error` | 422 | El valor enviado como estado queda fuera del catálogo `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. | | [`invalid_proforma_uuid`](/es/errors/invalid_proforma_uuid) | `invalid_request_error` | 400 | El identificador de proforma de la ruta o del payload no es un UUID válido. | | [`proforma_already_accepted`](/es/errors/proforma_already_accepted) | `invalid_request_error` | 422 | El cliente ya aceptó la proforma, y la aceptación se registra una sola vez. | | [`proforma_already_rejected`](/es/errors/proforma_already_rejected) | `invalid_request_error` | 422 | La proforma ya está marcada como rechazada. | | [`proforma_cannot_be_accepted`](/es/errors/proforma_cannot_be_accepted) | `invalid_request_error` | 422 | La aceptación no procede desde el estado actual: una proforma facturada, cancelada o expirada ya no la admite. | | [`proforma_cannot_be_rejected`](/es/errors/proforma_cannot_be_rejected) | `invalid_request_error` | 422 | El rechazo no procede desde el estado actual: una vez facturada, cancelada o expirada, la proforma está cerrada. | | [`proforma_cannot_be_sent`](/es/errors/proforma_cannot_be_sent) | `invalid_request_error` | 422 | El envío por email no aplica a una proforma en estado terminal: no hay oferta viva que entregar. | | [`proforma_invalid_status_transition`](/es/errors/proforma_invalid_status_transition) | `invalid_request_error` | 422 | El estado destino no es alcanzable desde el actual: un borrador se acepta, se cancela o expira; una proforma aceptada se factura, se rechaza o expira; facturada, cancelada y expirada son terminales. | | [`proforma_not_convertible_in_current_state`](/es/errors/proforma_not_convertible_in_current_state) | `invalid_request_error` | 422 | Convertir en factura exige que el cliente haya aceptado la proforma; desde cualquier otro estado no hay acuerdo que facturar. | | [`proforma_not_deletable_in_current_state`](/es/errors/proforma_not_deletable_in_current_state) | `invalid_request_error` | 422 | Solo se borra una proforma en borrador. Una vez aceptada, rechazada o facturada forma parte del rastro comercial. | | [`proforma_not_draft`](/es/errors/proforma_not_draft) | `invalid_request_error` | 422 | La operación solo tiene sentido mientras la proforma es un borrador, y esta ya ha avanzado. | | [`proforma_not_editable_in_current_state`](/es/errors/proforma_not_editable_in_current_state) | `invalid_request_error` | 422 | Solo una proforma en borrador admite edición. Una vez aceptada, rechazada, expirada, facturada o cancelada, su contenido queda fijado. | | [`proforma_not_found`](/es/errors/proforma_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna proforma de la empresa autenticada. | | [`proforma_requires_at_least_one_line`](/es/errors/proforma_requires_at_least_one_line) | `invalid_request_error` | 422 | La proforma no lleva líneas, así que no hay importe que poner delante del cliente. | | [`public_link_expires_at_exceeds_max_days`](/es/errors/public_link_expires_at_exceeds_max_days) | `invalid_request_error` | 422 | La caducidad pedida para el enlace público supera la ventana máxima que permite tu plan para documentos compartidos. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Facturas de compra (/es/errors/index-purchase-invoices) Códigos de error que emite Facturas de compra. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`attachment_invalid_filename`](/es/errors/attachment_invalid_filename) | `invalid_request_error` | 422 | El nombre del fichero no es utilizable: está vacío, lleva componentes de ruta, o supera los 200 caracteres. | | [`attachment_mime_not_allowed`](/es/errors/attachment_mime_not_allowed) | `invalid_request_error` | 422 | El tipo de fichero queda fuera del conjunto admitido: PDF, PNG, JPEG, XML y HTML. | | [`attachment_missing`](/es/errors/attachment_missing) | `not_found_error` | 404 | La factura de compra existe pero no tiene fichero adjunto, así que no hay nada que descargar. | | [`attachment_too_large`](/es/errors/attachment_too_large) | `invalid_request_error` | 422 | El fichero supera el tamaño máximo permitido para un adjunto de documento. | | [`cannot_attach_to_cancelled_purchase_invoice`](/es/errors/cannot_attach_to_cancelled_purchase_invoice) | `invalid_request_error` | 422 | La factura está cancelada, y adjuntar documentos a un registro cancelado alteraría documentación ya cerrada. | | [`invalid_purchase_invoice_id`](/es/errors/invalid_purchase_invoice_id) | `invalid_request_error` | 400 | La referencia de factura de compra recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. | | [`invalid_purchase_invoice_number`](/es/errors/invalid_purchase_invoice_number) | `invalid_request_error` | 422 | El número de factura está vacío o no encaja con el formato admitido. En una factura de compra el número es el que imprimió el proveedor, no uno que genere Factuarea. | | [`invalid_purchase_invoice_uuid`](/es/errors/invalid_purchase_invoice_uuid) | `invalid_request_error` | 400 | El identificador de factura de compra de la ruta o del payload no es un UUID válido. | | [`operation_regime_invalid`](/es/errors/operation_regime_invalid) | `invalid_request_error` | 422 | El régimen de operación queda fuera del catálogo `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. | | [`purchase_invoice_already_exists`](/es/errors/purchase_invoice_already_exists) | `conflict_error` | 409 | Ese proveedor ya tiene registrada una factura de compra con el mismo número. El par proveedor + número identifica el documento sin ambigüedad y evita contabilizar dos veces el mismo gasto. | | [`purchase_invoice_not_deletable_in_current_state`](/es/errors/purchase_invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Solo se borran las facturas de compra en borrador o canceladas. Una pendiente o pagada forma parte del libro de gastos. | | [`purchase_invoice_not_draft`](/es/errors/purchase_invoice_not_draft) | `invalid_request_error` | 422 | La operación solo aplica mientras la factura de compra es un borrador, y esta ya está registrada. | | [`purchase_invoice_not_editable_in_current_state`](/es/errors/purchase_invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Solo se edita una factura de compra en borrador. Una vez registrada como pendiente, pagada o cancelada, su contenido respalda un apunte contable. | | [`purchase_invoice_not_found`](/es/errors/purchase_invoice_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna factura de compra de la empresa autenticada. | | [`purchase_invoice_requires_at_least_one_line`](/es/errors/purchase_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura de compra no lleva líneas, así que no hay gasto ni IVA soportado que registrar. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Presupuestos (/es/errors/index-quotes) Códigos de error que emite Presupuestos. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------- | | [`quote_already_accepted`](/es/errors/quote_already_accepted) | `invalid_request_error` | 422 | El presupuesto ya estaba aprobado, y la aprobación se registra una sola vez. | | [`quote_already_rejected`](/es/errors/quote_already_rejected) | `invalid_request_error` | 422 | El presupuesto ya está marcado como rechazado. | | [`quote_expired`](/es/errors/quote_expired) | `invalid_request_error` | 422 | El presupuesto pasó su fecha de validez, así que las condiciones ofrecidas ya no vinculan y no se puede aprobar ni convertir tal cual. | | [`quote_not_found`](/es/errors/quote_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún presupuesto de la empresa autenticada. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Límite de tasa (/es/errors/index-rate-limit) Códigos de error que emite Límite de tasa. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ------------------ | ---- | ------------------------------------------------------------------------------- | | [`monthly_quota_exceeded`](/es/errors/monthly_quota_exceeded) | `rate_limit_error` | 429 | La empresa agotó la cuota mensual de llamadas que incluye su plan. | | [`rate_limit_exceeded`](/es/errors/rate_limit_exceeded) | `rate_limit_error` | 429 | La clave envió más peticiones de las que permite su ritmo en la ventana actual. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Facturas recurrentes (/es/errors/index-recurring-invoices) Códigos de error que emite Facturas recurrentes. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_frequency_interval`](/es/errors/invalid_frequency_interval) | `invalid_request_error` | 422 | El intervalo es menor que 1, así que la recurrencia nunca avanzaría a una siguiente ejecución. | | [`invalid_frequency_type`](/es/errors/invalid_frequency_type) | `invalid_request_error` | 422 | La frecuencia queda fuera del catálogo `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. | | [`invalid_holiday_handling`](/es/errors/invalid_holiday_handling) | `invalid_request_error` | 422 | La política de festivos queda fuera del catálogo `skip`, `before`, `after`, `same`. | | [`invalid_recurring_invoice_id`](/es/errors/invalid_recurring_invoice_id) | `invalid_request_error` | 400 | La referencia de recurrencia recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. | | [`invalid_recurring_invoice_uuid`](/es/errors/invalid_recurring_invoice_uuid) | `invalid_request_error` | 400 | El identificador de recurrencia de la ruta o del payload no es un UUID válido. | | [`recurring_already_active`](/es/errors/recurring_already_active) | `invalid_request_error` | 422 | La recurrencia ya está en marcha, así que no hay nada que activar. Código legacy conservado por compatibilidad: los endpoints actuales reportan esto como `recurring_invoice_already_active`. | | [`recurring_invoice_already_active`](/es/errors/recurring_invoice_already_active) | `invalid_request_error` | 422 | La recurrencia ya está en marcha. | | [`recurring_invoice_already_cancelled`](/es/errors/recurring_invoice_already_cancelled) | `invalid_request_error` | 422 | La recurrencia ya estaba cancelada, y la cancelación es terminal. | | [`recurring_invoice_already_paused`](/es/errors/recurring_invoice_already_paused) | `invalid_request_error` | 422 | La recurrencia ya está pausada, así que pausarla otra vez no cambia nada. | | [`recurring_invoice_cancelled_cannot_resume`](/es/errors/recurring_invoice_cancelled_cannot_resume) | `invalid_request_error` | 422 | Una recurrencia cancelada no se reanuda: la cancelación la cierra definitivamente, a diferencia de la pausa. | | [`recurring_invoice_cannot_run`](/es/errors/recurring_invoice_cannot_run) | `invalid_request_error` | 422 | La recurrencia no puede generar una factura ahora mismo: no está en marcha, su ciclo terminó, o le faltan datos que la factura necesita. `error.message` indica el motivo concreto. | | [`recurring_invoice_has_generated_invoices`](/es/errors/recurring_invoice_has_generated_invoices) | `invalid_request_error` | 422 | La recurrencia ya generó facturas, y esas facturas dependen de ella para su trazabilidad. | | [`recurring_invoice_not_found`](/es/errors/recurring_invoice_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna recurrencia de la empresa autenticada. | | [`recurring_invoice_requires_at_least_one_line`](/es/errors/recurring_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La recurrencia no lleva líneas, así que cada factura generada saldría vacía. | | [`recurring_not_active`](/es/errors/recurring_not_active) | `invalid_request_error` | 422 | La operación necesita una recurrencia en marcha y esta está pausada, completada o cancelada. Código legacy conservado por compatibilidad con integraciones antiguas. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Request (/es/errors/index-request) Códigos de error que emite Request. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`business_rule_violation`](/es/errors/business_rule_violation) | `invalid_request_error` | 422 | Una invariante del dominio rechazó la operación. Este código indica la familia; `error.subcode` nombra la regla concreta y `error.message` la explica. | | [`conflicting_pagination_params`](/es/errors/conflicting_pagination_params) | `invalid_request_error` | 422 | `starting_after` y `ending_before` viajaron en la misma petición. Recorren la colección en sentidos opuestos, así que solo puede aplicarse uno. | | [`external_id_already_exists`](/es/errors/external_id_already_exists) | `conflict_error` | 409 | El `external_id` con el que concilias contra tu sistema ya está asignado a otro objeto del mismo tipo en esta empresa. | | [`invalid_param_format`](/es/errors/invalid_param_format) | `invalid_request_error` | 422 | Un form request legacy rechazó la forma de un valor. Los endpoints migrados reportan lo mismo como `parameter_invalid_format` o `parameter_invalid_integer`. | | [`invalid_param_value`](/es/errors/invalid_param_value) | `invalid_request_error` | 422 | Un form request legacy rechazó el valor de un campo. Los endpoints migrados reportan lo mismo como `parameter_invalid_enum` o `parameter_invalid_range`. | | [`invalid_status_transition`](/es/errors/invalid_status_transition) | `invalid_request_error` | 422 | El estado solicitado no es alcanzable desde el estado en el que está ahora mismo el documento. | | [`length_required`](/es/errors/length_required) | `invalid_request_error` | 411 | Llegó una petición con body en codificación chunked, sin declarar su tamaño. La API necesita conocer la longitud por adelantado para rechazar payloads excesivos antes de cargarlos en memoria. | | [`metadata_too_many_keys`](/es/errors/metadata_too_many_keys) | `invalid_request_error` | 422 | El objeto `metadata` supera el límite de 50 claves por recurso. | | [`metadata_value_too_long`](/es/errors/metadata_value_too_long) | `invalid_request_error` | 422 | Un valor de `metadata` supera los 500 caracteres una vez serializado a texto. | | [`method_not_allowed`](/es/errors/method_not_allowed) | `invalid_request_error` | 405 | La ruta existe pero no acepta el verbo HTTP utilizado. | | [`missing_required_param`](/es/errors/missing_required_param) | `invalid_request_error` | 422 | Un form request legacy detectó que faltaba un campo obligatorio. Los endpoints ya migrados a los parsers canónicos reportan lo mismo como `parameter_missing`. | | [`parameter_invalid`](/es/errors/parameter_invalid) | `invalid_request_error` | 422 | Un value object construido a partir del payload rechazó el valor recibido. `error.subcode` dice cuál: código de impuesto, código de país, tipo impositivo, etc. | | [`parameter_invalid_boolean`](/es/errors/parameter_invalid_boolean) | `invalid_request_error` | 400 | Un parámetro que debe ser booleano recibió un valor fuera de las representaciones aceptadas (`true`/`false`, `1`/`0`). | | [`parameter_invalid_cursor`](/es/errors/parameter_invalid_cursor) | `invalid_request_error` | 400 | El cursor `starting_after` o `ending_before` no es un UUID válido, así que no puede apuntar a ninguna fila de la colección. | | [`parameter_invalid_empty`](/es/errors/parameter_invalid_empty) | `invalid_request_error` | 400 | Un parámetro llegó con el valor vacío: un filtro `in` sin elementos, una comparación sin nada tras el operador, o un filtro de igualdad con la cadena vacía. | | [`parameter_invalid_enum`](/es/errors/parameter_invalid_enum) | `invalid_request_error` | 400 | El valor queda fuera del conjunto cerrado que acepta el parámetro. En los listados cubre además un operador de filtro distinto de `eq`, `gte`, `lte`, `gt`, `lt`, `in` o `contains`. | | [`parameter_invalid_format`](/es/errors/parameter_invalid_format) | `invalid_request_error` | 400 | El valor tiene el tipo correcto pero no la forma que exige el parámetro: una fecha, un patrón de identificador o una cabecera como `Factuarea-Version`. | | [`parameter_invalid_integer`](/es/errors/parameter_invalid_integer) | `invalid_request_error` | 400 | Un parámetro que debe ser un número entero recibió algo que no se puede interpretar como tal, por ejemplo `limit=abc`. | | [`parameter_invalid_iso8601`](/es/errors/parameter_invalid_iso8601) | `invalid_request_error` | 400 | Un filtro de rango (`gte`, `lte`, `gt`, `lt`) recibió un valor que no es numérico ni una fecha ISO 8601. | | [`parameter_invalid_range`](/es/errors/parameter_invalid_range) | `invalid_request_error` | 400 | Un parámetro numérico quedó fuera de sus límites. El caso habitual es `limit`, que debe estar entre 1 y 100. | | [`parameter_invalid_string`](/es/errors/parameter_invalid_string) | `invalid_request_error` | 400 | Un parámetro que debe ser texto recibió un array, un objeto o un valor que no se puede leer como cadena. | | [`parameter_invalid_url`](/es/errors/parameter_invalid_url) | `invalid_request_error` | 400 | Un campo que debe contener una URL absoluta recibió un valor que no lo es, normalmente por faltarle el esquema o el host. | | [`parameter_invalid_uuid`](/es/errors/parameter_invalid_uuid) | `invalid_request_error` | 400 | Un campo de identificador recibió un valor que no es un UUID válido. Todo `id` de recurso en v1 es un UUID. | | [`parameter_invalid_value`](/es/errors/parameter_invalid_value) | `invalid_request_error` | 422 | El valor es sintácticamente correcto pero no admisible para este recurso: fuera del catálogo canónico del campo, o incoherente con el resto del payload. | | [`parameter_missing`](/es/errors/parameter_missing) | `invalid_request_error` | 400 | El endpoint exige un parámetro que la petición no llevaba. `error.param` dice cuál. | | [`parameter_unknown`](/es/errors/parameter_unknown) | `invalid_request_error` | 400 | La petición lleva un parámetro que el endpoint no acepta: un filtro fuera de su allowlist, un campo de `sort` no ordenable, o el `page` de paginación por offset — v1 pagina por cursor. | | [`payload_too_large`](/es/errors/payload_too_large) | `invalid_request_error` | 413 | El body de la petición supera el tamaño admitido: 1 MB con carácter general, 6 MB en los endpoints que aceptan ficheros. | | [`profile_not_found`](/es/errors/profile_not_found) | `not_found_error` | 404 | La cabecera `X-Active-Profile` nombra una empresa que no existe o que no pertenece al árbol de gestoría de la clave autenticada. Ambos casos responden igual para que la API nunca revele empresas de otros tenants. | | [`resource_already_exists`](/es/errors/resource_already_exists) | `conflict_error` | 409 | Crear el objeto duplicaría uno que ya existe bajo una clave única — NIF, SKU, external id. `error.details.existing_resource_id` apunta al objeto que ya ocupa ese valor. | | [`resource_conflict`](/es/errors/resource_conflict) | `conflict_error` | 409 | La operación chocó con el estado actual del recurso y no aplica ningún código de conflicto más específico. | | [`resource_immutable`](/es/errors/resource_immutable) | `invalid_request_error` | 422 | El objeto está cerrado a cambios para esta operación: su estado o su registro contable impiden modificarlo. | | [`resource_locked`](/es/errors/resource_locked) | `conflict_error` | 409 | Otra operación retiene el recurso hasta terminar: las escrituras concurrentes sobre el mismo objeto se serializan en lugar de entrelazarse. | | [`resource_not_deletable`](/es/errors/resource_not_deletable) | `invalid_request_error` | 422 | El objeto existe, pero su estado o sus dependientes bloquean el borrado. En los borrados masivos este es el código por fila de cada entrada que no se pudo eliminar. | | [`resource_not_found`](/es/errors/resource_not_found) | `not_found_error` | 404 | El identificador no resuelve a nada visible para la empresa autenticada. Los objetos de otra empresa responden exactamente igual, a propósito. | | [`route_not_found`](/es/errors/route_not_found) | `not_found_error` | 404 | La ruta no corresponde a ningún endpoint de v1. Suele ser una errata, un prefijo `/v1` ausente o una ruta de otra área de la API. | | [`unknown_filter`](/es/errors/unknown_filter) | `invalid_request_error` | 422 | Un listado recibió un filtro que no conoce. Los parsers canónicos de v1 reportan esto como `parameter_unknown`; este código sobrevive para los endpoints aún sin migrar. | | [`unsupported_api_version`](/es/errors/unsupported_api_version) | `invalid_request_error` | 400 | La cabecera `Factuarea-Version` está bien formada pero nombra una versión fuera del conjunto soportado. | | [`unsupported_media_type`](/es/errors/unsupported_media_type) | `invalid_request_error` | 415 | Una petición con body declaró un `Content-Type` distinto de `application/json`. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Series (/es/errors/index-series) Códigos de error que emite Series. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`cannot_archive_last_default_series`](/es/errors/cannot_archive_last_default_series) | `invalid_request_error` | 422 | La serie es la única activa de su tipo de documento. Archivarla dejaría a la empresa sin numeración disponible y congelaría ese tipo de documento. | | [`document_type_required_for_ambiguous_code`](/es/errors/document_type_required_for_ambiguous_code) | `invalid_request_error` | 422 | Ese código de serie existe para más de un tipo de documento, así que por sí solo no identifica una única serie. | | [`invalid_series_code`](/es/errors/invalid_series_code) | `invalid_request_error` | 422 | El código de la serie está vacío, es demasiado largo, o lleva caracteres que no corresponden a un prefijo fiscal. | | [`invalid_series_name`](/es/errors/invalid_series_name) | `invalid_request_error` | 422 | El nombre de la serie está vacío o supera la longitud permitida. | | [`invalid_series_number`](/es/errors/invalid_series_number) | `invalid_request_error` | 422 | El número inicial no es válido: no es un entero positivo, o queda en el último número ya emitido o por debajo, lo que reemitiría números ya consumidos. | | [`invalid_series_uuid`](/es/errors/invalid_series_uuid) | `invalid_request_error` | 400 | El identificador de serie de la ruta o del payload no es un UUID válido. | | [`invalid_series_year`](/es/errors/invalid_series_year) | `invalid_request_error` | 422 | El ejercicio no es un año de cuatro cifras válido para una serie de numeración. | | [`monthly_requires_month_segmented_format`](/es/errors/monthly_requires_month_segmented_format) | `invalid_request_error` | 422 | El contador se reinicia cada mes pero la máscara de numeración no segrega por mes, así que dos meses arrancarían en el mismo correlativo y producirían números duplicados dentro del año. | | [`series_already_archived`](/es/errors/series_already_archived) | `invalid_request_error` | 422 | La serie ya estaba archivada, y el archivado no se repite: una segunda llamada indica que el cliente ha perdido el estado real. | | [`series_code_immutable_with_documents`](/es/errors/series_code_immutable_with_documents) | `invalid_request_error` | 422 | Cambiar el prefijo de una serie que ya emitió documentos reescribiría retroactivamente su identificador fiscal, mientras los clientes y la AEAT tienen el número original. | | [`series_has_documents`](/es/errors/series_has_documents) | `invalid_request_error` | 422 | La serie ya numeró documentos, así que no se puede eliminar: la secuencia correlativa tiene que seguir siendo auditable. | | [`series_immutable`](/es/errors/series_immutable) | `invalid_request_error` | 405 | Las series no son editables ni eliminables vía API: la continuidad legal de la numeración exige que su prefijo, su año y su contador se queden como están. | | [`series_initial_number_creates_gap`](/es/errors/series_initial_number_creates_gap) | `invalid_request_error` | 422 | El número inicial salta más allá del siguiente correlativo natural habiendo documentos del año en curso, y ese hueco en la secuencia no es admisible para la AEAT. | | [`series_locked_by_verifactu`](/es/errors/series_locked_by_verifactu) | `invalid_request_error` | 422 | Al menos una factura de la serie tiene un registro de facturación aceptado por la AEAT, lo que congela el prefijo, el año y la base de numeración de la serie. | | [`series_not_found`](/es/errors/series_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna serie de numeración de la empresa autenticada. | | [`series_type_invalid`](/es/errors/series_type_invalid) | `invalid_request_error` | 422 | El tipo de documento de la serie queda fuera del catálogo `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`series_year_locked`](/es/errors/series_year_locked) | `invalid_request_error` | 422 | La serie ya emitió documentos en su año vigente. Mover el año dejaría esos documentos apuntando a un ejercicio vacío mientras su base imponible está en otro. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Servidor (/es/errors/index-server) Códigos de error que emite Servidor. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------- | --------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`dependency_unavailable`](/es/errors/dependency_unavailable) | `service_unavailable_error` | 503 | Un servicio externo del que depende la operación no respondió a tiempo. | | [`face_transmission_failed`](/es/errors/face_transmission_failed) | `api_error` | 502 | La plataforma FACe —el punto de entrada de las administraciones públicas— estaba inaccesible o respondió con un fallo. El problema está aguas arriba, no en tu petición. | | [`facturae_signing_failed`](/es/errors/facturae_signing_failed) | `api_error` | 500 | No se pudo producir la firma XAdES del fichero Facturae, normalmente porque el certificado de firma no es utilizable en ese momento. | | [`internal_error`](/es/errors/internal_error) | `api_error` | 500 | Algo se rompió en nuestro lado al procesar la petición. La condición no la provoca tu payload. | | [`maintenance`](/es/errors/maintenance) | `service_unavailable_error` | 503 | La plataforma está en ventana de mantenimiento y las escrituras se retienen a propósito. | | [`pdf_generation_failed`](/es/errors/pdf_generation_failed) | `service_unavailable_error` | 503 | El servicio de renderizado no pudo producir el PDF. El documento y sus datos están intactos: lo que falló es el fichero. | | [`register_sealing_failed`](/es/errors/register_sealing_failed) | `api_error` | 500 | El sellado criptográfico del registro no se completó, así que el cierre quedó sin firmar en lugar de sellado con una firma rota. | | [`send_failed`](/es/errors/send_failed) | `api_error` | 500 | El documento no se entregó por email: el proveedor de correo rechazó el mensaje o estaba inaccesible. | | [`service_unavailable`](/es/errors/service_unavailable) | `service_unavailable_error` | 503 | El servicio, o una dependencia que necesita, no puede responder temporalmente. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Proveedores (/es/errors/index-suppliers) Códigos de error que emite Proveedores. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | | [`supplier_has_documents`](/es/errors/supplier_has_documents) | `invalid_request_error` | 422 | El proveedor está referenciado por facturas de compra registradas, y borrarlo dejaría esos gastos sin la parte que los emitió. | | [`supplier_not_found`](/es/errors/supplier_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún proveedor de la empresa autenticada. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Informes fiscales (/es/errors/index-tax-reports) Códigos de error que emite Informes fiscales. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`insufficient_data_for_report`](/es/errors/insufficient_data_for_report) | `invalid_request_error` | 422 | El período no tiene datos que declarar, o a una factura del período le falta un campo obligatorio para este modelo, típicamente el NIF del cliente. | | [`invalid_period`](/es/errors/invalid_period) | `invalid_request_error` | 422 | El período no identifica una declaración: el año queda fuera del rango admitido, o falta el trimestre o está fuera del rango 1 a 4 en un modelo trimestral. | | [`report_format_invalid`](/es/errors/report_format_invalid) | `invalid_request_error` | 422 | El formato queda fuera del catálogo `txt_aeat`, `pdf`, `excel`. | | [`tax_report_not_found`](/es/errors/tax_report_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna declaración de la empresa autenticada. | | [`tax_report_type_invalid`](/es/errors/tax_report_type_invalid) | `invalid_request_error` | 422 | El tipo de declaración queda fuera del catálogo `modelo_303`, `modelo_347`, `modelo_130`. | | [`unsupported_format`](/es/errors/unsupported_format) | `invalid_request_error` | 422 | El formato pedido no está disponible para este modelo: no toda declaración produce todas las salidas. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Impuestos (/es/errors/index-taxes) Códigos de error que emite Impuestos. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`custom_tax_creation_disabled`](/es/errors/custom_tax_creation_disabled) | `authorization_error` | 403 | La creación de impuestos personalizados está deshabilitada para esta empresa. | | [`duplicate_tax_default_for_document_type`](/es/errors/duplicate_tax_default_for_document_type) | `invalid_request_error` | 422 | Ya hay otro impuesto del mismo tipo marcado como default para ese tipo de documento, y el par (tipo de impuesto, tipo de documento) admite un único default. | | [`indirect_tax_regime_invalid`](/es/errors/indirect_tax_regime_invalid) | `invalid_request_error` | 422 | El régimen indirecto queda fuera del catálogo `iva`, `igic`, `ipsi`. | | [`invalid_aeat_code`](/es/errors/invalid_aeat_code) | `invalid_request_error` | 422 | El código de operación AEAT queda fuera del catálogo cerrado `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` que usan VeriFactu y el SII. | | [`invalid_country_aeat_zone`](/es/errors/invalid_country_aeat_zone) | `invalid_request_error` | 422 | La zona territorial AEAT queda fuera del catálogo `peninsula`, `canarias`, `ceuta`, `melilla`. | | [`invalid_country_code`](/es/errors/invalid_country_code) | `invalid_request_error` | 422 | El código de país no tiene exactamente dos caracteres, así que no es un código ISO 3166-1 alfa-2 válido. | | [`invalid_customer_visible_label`](/es/errors/invalid_customer_visible_label) | `invalid_request_error` | 422 | La etiqueta que se muestra al cliente en el documento supera la longitud permitida. | | [`invalid_description`](/es/errors/invalid_description) | `invalid_request_error` | 422 | La descripción supera la longitud máxima permitida para el campo. | | [`invalid_document_type`](/es/errors/invalid_document_type) | `invalid_request_error` | 422 | El tipo de documento queda fuera del catálogo: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`invalid_rate_for_tax_regime`](/es/errors/invalid_rate_for_tax_regime) | `invalid_request_error` | 422 | El tipo no pertenece a la rejilla legal de su régimen: el IGIC admite 0, 3, 5, 7, 9,5, 15 y 20 %; el IPSI admite 0, 0,5, 1, 2, 4, 8 y 10 %. | | [`invalid_tax_code`](/es/errors/invalid_tax_code) | `invalid_request_error` | 422 | El código del impuesto está vacío o supera los 50 caracteres. | | [`invalid_tax_name`](/es/errors/invalid_tax_name) | `invalid_request_error` | 422 | El nombre del impuesto está vacío o supera los 255 caracteres. | | [`invalid_tax_rate`](/es/errors/invalid_tax_rate) | `invalid_request_error` | 422 | El tipo impositivo queda fuera del rango permitido para su clase: IVA 0-27 %, retención 0-47 %, recargo de equivalencia 0-10 %, otros 0-100 %. | | [`invalid_tax_type_filter`](/es/errors/invalid_tax_type_filter) | `invalid_request_error` | 422 | El filtro `type` del listado por tipo lleva un valor fuera del enum `vat`, `retention`, `surcharge`, `other`. | | [`invalid_validity_window`](/es/errors/invalid_validity_window) | `invalid_request_error` | 422 | La ventana de vigencia está invertida: `valid_until` es anterior a `valid_from`. | | [`system_tax_default_modification_forbidden`](/es/errors/system_tax_default_modification_forbidden) | `authorization_error` | 403 | Los defaults de los impuestos del catálogo compartido no se fijan sobre el impuesto: el catálogo es global y la preferencia es de tu empresa. | | [`system_tax_immutable`](/es/errors/system_tax_immutable) | `invalid_request_error` | 422 | El impuesto pertenece al catálogo canónico AEAT que trae el producto. Su tipo, su código y su nombre son fijos para que todas las empresas compartan la misma referencia fiscal. | | [`system_tax_immutable_field`](/es/errors/system_tax_immutable_field) | `invalid_request_error` | 422 | La actualización toca un campo congelado en un impuesto del sistema; `error.param` dice cuál. | | [`system_tax_undeletable`](/es/errors/system_tax_undeletable) | `invalid_request_error` | 422 | Los impuestos del sistema forman parte del catálogo fiscal compartido y no se eliminan: borrarlos rompería los documentos que los referencian. | | [`tax_applies_to_invalid`](/es/errors/tax_applies_to_invalid) | `invalid_request_error` | 422 | El ámbito del impuesto queda fuera del catálogo `sale`, `purchase`, `both`. | | [`tax_code_already_exists`](/es/errors/tax_code_already_exists) | `conflict_error` | 409 | Otro impuesto del catálogo ya usa ese código, y el código identifica al impuesto sin ambigüedad. | | [`tax_id_required`](/es/errors/tax_id_required) | `invalid_request_error` | 422 | La operación necesita el número de identificación fiscal (NIF, CIF o NIE) de la parte implicada y el registro no lo tiene. | | [`tax_in_use`](/es/errors/tax_in_use) | `invalid_request_error` | 422 | El impuesto está referenciado por documentos, productos o proveedores. Eliminarlo dejaría documentos históricos sin su referencia fiscal. | | [`tax_inactive_cannot_be_default`](/es/errors/tax_inactive_cannot_be_default) | `invalid_request_error` | 422 | Un impuesto desactivado no puede quedar como default, ni global ni por tipo de documento: sería un default oculto que ningún formulario puede elegir. | | [`tax_not_found`](/es/errors/tax_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún impuesto del catálogo accesible para esta empresa. | | [`tax_type_invalid`](/es/errors/tax_type_invalid) | `invalid_request_error` | 422 | El tipo de impuesto queda fuera del catálogo `vat`, `retention`, `surcharge`, `other`. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de VeriFactu (/es/errors/index-verifactu) Códigos de error que emite VeriFactu. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`alta_record_not_found`](/es/errors/alta_record_not_found) | `not_found_error` | 404 | La factura no tiene registro de alta, así que la operación que depende de él no tiene sobre qué trabajar. | | [`anulacion_record_already_exists`](/es/errors/anulacion_record_already_exists) | `conflict_error` | 409 | La factura ya tiene un registro de anulación en la cadena, y la anulación se declara una sola vez. | | [`certificate_expired`](/es/errors/certificate_expired) | `invalid_request_error` | 422 | El certificado está fuera de su ventana de validez: ha caducado, o todavía no es válido. | | [`certificate_nif_mismatch`](/es/errors/certificate_nif_mismatch) | `invalid_request_error` | 422 | El NIF del titular del certificado no coincide con el de la empresa. Los registros AEAT se firman en nombre de la empresa, así que ambos deben ser el mismo. | | [`certificate_not_found`](/es/errors/certificate_not_found) | `not_found_error` | 404 | La empresa no tiene ningún certificado FNMT que corresponda al identificador, o no tiene ninguno subido. | | [`certificate_too_large`](/es/errors/certificate_too_large) | `invalid_request_error` | 422 | El fichero supera el límite de 100 KB, cuando un certificado FNMT real pesa unos pocos kilobytes. | | [`clock_drift_exceeded`](/es/errors/clock_drift_exceeded) | `invalid_request_error` | 422 | El reloj del servidor se desvió del NTP por encima del margen permitido. La marca de tiempo de generación entra en la huella AEAT, así que un reloj desincronizado produciría registros que la AEAT rechaza. | | [`declaracion_already_exists`](/es/errors/declaracion_already_exists) | `conflict_error` | 409 | La empresa ya tiene presentada la declaración responsable del SIF de ese período. | | [`declaracion_not_found`](/es/errors/declaracion_not_found) | `not_found_error` | 404 | La empresa no tiene presentada la declaración responsable del SIF del período solicitado. | | [`event_already_processed`](/es/errors/event_already_processed) | `invalid_request_error` | 422 | Ese evento del SIF ya está registrado en la cadena de eventos, y cada evento se procesa exactamente una vez. | | [`invalid_certificate_format`](/es/errors/invalid_certificate_format) | `invalid_request_error` | 422 | El fichero no es un contenedor PKCS#12: sus primeros bytes no corresponden a la estructura ASN.1 que exige el formato, diga lo que diga la extensión. | | [`invalid_certificate_password`](/es/errors/invalid_certificate_password) | `invalid_request_error` | 422 | La contraseña no abre el fichero del certificado. | | [`max_retries_exceeded`](/es/errors/max_retries_exceeded) | `invalid_request_error` | 422 | El registro agotó el presupuesto de reintentos técnicos de reenvío del XML almacenado. Reintentar el mismo contenido volvería a fallar igual. | | [`mode_switch_blocked_until_year_end`](/es/errors/mode_switch_blocked_until_year_end) | `invalid_request_error` | 422 | El modo VeriFactu se activó en este ejercicio y ya se emitió al menos un registro de facturación. Dar marcha atrás degradaría la integridad de una cadena ya declarada a la AEAT. | | [`record_already_accepted`](/es/errors/record_already_accepted) | `invalid_request_error` | 422 | La AEAT ya aceptó el registro. La aceptación es terminal y su contenido queda congelado como parte de la cadena de huellas. | | [`record_immutable`](/es/errors/record_immutable) | `invalid_request_error` | 422 | El registro pertenece a un ledger de solo-adición: una vez escrito, su contenido fiscal queda cerrado a modificaciones y a borrado. | | [`record_not_rejected`](/es/errors/record_not_rejected) | `invalid_request_error` | 422 | La subsanación solo aplica a registros que la AEAT rechazó por datos. Este registro está en otro estado — un fallo técnico, por ejemplo, lo cubre el reintento automático. | | [`record_not_subsanable`](/es/errors/record_not_subsanable) | `invalid_request_error` | 422 | El registro no se puede subsanar: no es un registro de alta, o no tiene factura de origen desde la que regenerar su contenido. | | [`requires_annulment`](/es/errors/requires_annulment) | `invalid_request_error` | 422 | El contenido regenerado cambia un campo que entra en la huella —NIF del emisor, serie y número, fecha de expedición, tipo de factura, cuota o importe total— y la cadena no se puede reescribir. | | [`sii_excluded`](/es/errors/sii_excluded) | `invalid_request_error` | 422 | La empresa está registrada en el SII, y los obligados al SII quedan excluidos del reglamento VeriFactu. | | [`verifactu_already_submitted`](/es/errors/verifactu_already_submitted) | `invalid_request_error` | 422 | La factura ya tiene su registro de alta. Existe exactamente un alta por factura, así que una segunda rompería la idempotencia de la cadena. | | [`verifactu_mode_invalid`](/es/errors/verifactu_mode_invalid) | `invalid_request_error` | 422 | El modo queda fuera del catálogo `verifactu` / `no_verifactu`. | | [`verifactu_not_eligible`](/es/errors/verifactu_not_eligible) | `invalid_request_error` | 422 | La factura no se puede registrar ahora mismo en la AEAT: la empresa no está en modo VeriFactu, no tiene certificado activo, o el certificado está revocado o emitido para otro NIF. | | [`verifactu_record_not_found`](/es/errors/verifactu_record_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún registro de facturación de la empresa autenticada. | | [`verifactu_transmission_failed`](/es/errors/verifactu_transmission_failed) | `invalid_request_error` | 422 | El envío del registro a la AEAT no llegó a completarse: el endpoint estaba inaccesible o respondió con una incidencia. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # Códigos de error de Webhooks (/es/errors/index-webhooks) Códigos de error que emite Webhooks. Cada `code` enlaza a su propia página con la causa y la acción a tomar. | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_required`](/es/errors/addon_required) | `payment_required_error` | 402 | Crear endpoints de webhook pertenece al add-on Developer API, y la empresa no lo tiene activo: el nivel gratuito permite cero endpoints. | | [`api_version_invalid_format`](/es/errors/api_version_invalid_format) | `invalid_request_error` | 422 | La versión de payload del endpoint no es una fecha `YYYY-MM-DD`. | | [`api_version_unsupported`](/es/errors/api_version_unsupported) | `invalid_request_error` | 422 | La versión de payload está bien formada pero no está entre las que sirve la plataforma. | | [`custom_header_blocklisted`](/es/errors/custom_header_blocklisted) | `invalid_request_error` | 422 | Una de las cabeceras personalizadas está reservada: la gestiona la capa HTTP (`host`, `content-type`, `content-length`, `user-agent`), la envía Factuarea como parte del contrato firmado (`factuarea-*`), o pertenece al proxy (`x-forwarded-*`). | | [`custom_header_value_too_long`](/es/errors/custom_header_value_too_long) | `invalid_request_error` | 422 | El valor de una cabecera personalizada supera los 1024 caracteres. | | [`replay_delivery_not_retryable`](/es/errors/replay_delivery_not_retryable) | `invalid_request_error` | 422 | Solo se reenvían las entregas fallidas. Una entrega que llegó bien, o una todavía en curso, no tiene nada que reenviar. | | [`replay_event_expired`](/es/errors/replay_event_expired) | `invalid_request_error` | 422 | El evento que respalda la entrega fue purgado por la política de retención de 30 días, así que ya no queda payload que reenviar. | | [`timeout_seconds_out_of_range`](/es/errors/timeout_seconds_out_of_range) | `invalid_request_error` | 422 | `timeout_seconds` queda fuera del rango de 1 a 30 segundos. | | [`too_many_custom_headers`](/es/errors/too_many_custom_headers) | `invalid_request_error` | 422 | El endpoint declara más de 20 cabeceras personalizadas. | | [`webhook_delivery_not_found`](/es/errors/webhook_delivery_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún intento de entrega, o la entrega queda fuera de la ventana de retención del histórico. | | [`webhook_endpoint_degraded`](/es/errors/webhook_endpoint_degraded) | `invalid_request_error` | 422 | El endpoint está degradado tras fallos repetidos de entrega, así que los pings de prueba se rechazan mientras siga en ese estado. | | [`webhook_endpoint_not_found`](/es/errors/webhook_endpoint_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún endpoint de webhook de la empresa autenticada. | | [`webhook_secret_recently_rotated`](/es/errors/webhook_secret_recently_rotated) | `rate_limit_error` | 429 | El secreto de firma se rotó hace menos de cinco minutos. La ventana de gracia permite que tu receptor acepte ambos secretos durante el cambio; rotar otra vez dentro de ella invalidaría firmas todavía en vuelo. | ## Relacionado [#relacionado] * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # indirect_tax_regime_invalid (/es/errors/indirect_tax_regime_invalid) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | ----------------------------------- | | `indirect_tax_regime_invalid` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El régimen indirecto queda fuera del catálogo `iva`, `igic`, `ipsi`. ## Qué hacer [#qué-hacer] Envía uno de los tres regímenes, o deja que se derive de la zona AEAT en lugar de declararlo a mano. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # insufficient_data_for_report (/es/errors/insufficient_data_for_report) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ------------------------------------------------- | | `insufficient_data_for_report` | `invalid_request_error` | 422 | [Informes fiscales](/es/errors/index-tax-reports) | ## Causa [#causa] El período no tiene datos que declarar, o a una factura del período le falta un campo obligatorio para este modelo, típicamente el NIF del cliente. ## Qué hacer [#qué-hacer] Lee `error.subcode`: completa el dato que falta en las facturas que señala, o elige un período con actividad. ## Relacionado [#relacionado] * [Todos los códigos de error de Informes fiscales](/es/errors/index-tax-reports) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # insufficient_scope (/es/errors/insufficient_scope) | Code | Type | HTTP | Categoría | | -------------------- | --------------------- | ---- | ---------------------------------------------- | | `insufficient_scope` | `authorization_error` | 403 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] La clave autentica correctamente pero no lleva el scope que exige esta operación. Los scopes se conceden al emitir la clave y no se amplían en tiempo de llamada. ## Qué hacer [#qué-hacer] Emite una clave que incluya el scope que indica `error.message` —de lectura para consultas, de escritura para cambios— y úsala para esta llamada. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # internal_error (/es/errors/internal_error) | Code | Type | HTTP | Categoría | | ---------------- | ----------- | ---- | ----------------------------------- | | `internal_error` | `api_error` | 500 | [Servidor](/es/errors/index-server) | ## Causa [#causa] Algo se rompió en nuestro lado al procesar la petición. La condición no la provoca tu payload. ## Qué hacer [#qué-hacer] Reintenta con backoff exponencial, reutilizando la misma `Idempotency-Key` en las escrituras, y comunica el `request_id` si persiste. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_aeat_code (/es/errors/invalid_aeat_code) | Code | Type | HTTP | Categoría | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_aeat_code` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El código de operación AEAT queda fuera del catálogo cerrado `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` que usan VeriFactu y el SII. ## Qué hacer [#qué-hacer] Elige el código que corresponde a la naturaleza fiscal de la operación: `S1` sujeta y no exenta, `S2` inversión del sujeto pasivo, `E1`-`E6` exenciones, `N1`-`N2` no sujeta. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_api_key (/es/errors/invalid_api_key) | Code | Type | HTTP | Categoría | | ----------------- | ---------------------- | ---- | ------------------------------------------------ | | `invalid_api_key` | `authentication_error` | 401 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] La clave no corresponde a ninguna clave activa. Puede estar mal copiada, truncada, o pertenecer a otro entorno: las claves de prueba y las de producción no son intercambiables. ## Qué hacer [#qué-hacer] Vuelve a copiar la clave del panel y comprueba el entorno: las `fact_test_` solo funcionan en modo test y las `fact_live_` solo en producción. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > API key inválida. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_certificate_format (/es/errors/invalid_certificate_format) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------------- | ---- | --------------------------------------- | | `invalid_certificate_format` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El fichero no es un contenedor PKCS#12: sus primeros bytes no corresponden a la estructura ASN.1 que exige el formato, diga lo que diga la extensión. ## Qué hacer [#qué-hacer] Sube el fichero `.p12` o `.pfx` original; un PEM, un CRT o un fichero renombrado no se aceptan. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_certificate_password (/es/errors/invalid_certificate_password) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | --------------------------------------- | | `invalid_certificate_password` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La contraseña no abre el fichero del certificado. ## Qué hacer [#qué-hacer] Envía la contraseña que protege el `.p12` tal cual se fijó: los espacios y las mayúsculas cuentan. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_correction_nature (/es/errors/invalid_correction_nature) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_correction_nature` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] `correction_nature` solo acepta `S` (sustitución: la rectificativa lleva los importes corregidos completos) o `I` (por diferencias: lleva solo el delta). ## Qué hacer [#qué-hacer] Envía `S` cuando la rectificativa sustituye los importes del original, e `I` cuando solo recoge la diferencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_correction_reason (/es/errors/invalid_correction_reason) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_correction_reason` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El motivo de rectificación queda fuera de la lista fiscal cerrada (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), que mapea a los códigos AEAT R1 a R4. ## Qué hacer [#qué-hacer] Elige el motivo que refleje la causa real: decide el código que se declara a la AEAT, y `concurso` e `incobrable` exigen documentación acreditativa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_country_aeat_zone (/es/errors/invalid_country_aeat_zone) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_country_aeat_zone` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La zona territorial AEAT queda fuera del catálogo `peninsula`, `canarias`, `ceuta`, `melilla`. ## Qué hacer [#qué-hacer] Envía la zona que corresponde al territorio del impuesto: decide el régimen indirecto (IVA, IGIC o IPSI) y la rejilla legal de tipos. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_country_code (/es/errors/invalid_country_code) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_country_code` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El código de país no tiene exactamente dos caracteres, así que no es un código ISO 3166-1 alfa-2 válido. ## Qué hacer [#qué-hacer] Envía el código de dos letras del país (`ES`, `FR`, `PT`). ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_customer_visible_label (/es/errors/invalid_customer_visible_label) | Code | Type | HTTP | Categoría | | -------------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_customer_visible_label` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La etiqueta que se muestra al cliente en el documento supera la longitud permitida. ## Qué hacer [#qué-hacer] Acorta la etiqueta: está pensada como rótulo breve en la línea del documento, no como descripción. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_description (/es/errors/invalid_description) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_description` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La descripción supera la longitud máxima permitida para el campo. ## Qué hacer [#qué-hacer] Acorta la descripción; el detalle identificativo va en el nombre y el código, no aquí. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_document_type (/es/errors/invalid_document_type) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_document_type` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El tipo de documento queda fuera del catálogo: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. ## Qué hacer [#qué-hacer] Envía uno de esos valores en el campo que selecciona el tipo de documento. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_expiry_date (/es/errors/invalid_expiry_date) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_expiry_date` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La fecha de vencimiento es anterior a la de emisión, o la supera en más de 365 días. ## Qué hacer [#qué-hacer] Envía una fecha de vencimiento entre la fecha de emisión y 365 días después. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_frequency_interval (/es/errors/invalid_frequency_interval) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `invalid_frequency_interval` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] El intervalo es menor que 1, así que la recurrencia nunca avanzaría a una siguiente ejecución. ## Qué hacer [#qué-hacer] Envía un intervalo de 1 o más: multiplica a la frecuencia, como `monthly` con intervalo 2 para cada dos meses. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_frequency_type (/es/errors/invalid_frequency_type) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------------------------------- | | `invalid_frequency_type` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La frecuencia queda fuera del catálogo `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. ## Qué hacer [#qué-hacer] Elige una de las frecuencias; usa `custom` con un intervalo explícito cuando ninguna de las nombradas encaje. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_holiday_handling (/es/errors/invalid_holiday_handling) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `invalid_holiday_handling` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La política de festivos queda fuera del catálogo `skip`, `before`, `after`, `same`. ## Qué hacer [#qué-hacer] Elige qué debe pasar cuando una ejecución cae en festivo: saltarla, adelantarla, retrasarla, o emitir igualmente en esa fecha. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_invoice_id (/es/errors/invalid_invoice_id) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_id` | `invalid_request_error` | 400 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La referencia de factura recibida no es un identificador válido; suele significar que se coló un valor interno donde la API espera el `id` público. ## Qué hacer [#qué-hacer] Envía el `id` de factura que devuelve la API; los identificadores numéricos internos no forman parte del contrato v1. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_invoice_number (/es/errors/invalid_invoice_number) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_number` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El número de factura no sigue el formato canónico `SERIE-AAAA-NNN`, más el sufijo `-RECn` en las rectificativas. ## Qué hacer [#qué-hacer] Envía el número tal cual aparece en la factura, en lugar de componerlo a partir de sus partes. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_invoice_status (/es/errors/invalid_invoice_status) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_status` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El valor enviado como estado de factura queda fuera del catálogo del ciclo de vida (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). ## Qué hacer [#qué-hacer] Usa uno de los valores del catálogo, escrito exactamente como lo devuelve la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_invoice_uuid (/es/errors/invalid_invoice_uuid) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `invalid_invoice_uuid` | `invalid_request_error` | 400 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El identificador de factura de la ruta o del payload no es un UUID válido. ## Qué hacer [#qué-hacer] Copia el `id` exactamente como lo devolvió la API, sin truncarlo ni recodificarlo. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_param_format (/es/errors/invalid_param_format) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_param_format` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Un form request legacy rechazó la forma de un valor. Los endpoints migrados reportan lo mismo como `parameter_invalid_format` o `parameter_invalid_integer`. ## Qué hacer [#qué-hacer] Corrige el formato del campo de `error.param`; si ramificas por código de error, trata este como alias de `parameter_invalid_format`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_param_value (/es/errors/invalid_param_value) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_param_value` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Un form request legacy rechazó el valor de un campo. Los endpoints migrados reportan lo mismo como `parameter_invalid_enum` o `parameter_invalid_range`. ## Qué hacer [#qué-hacer] Corrige el valor de `error.param`; si ramificas por código de error, trata este como alias de `parameter_invalid_enum`. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El valor de uno o más parámetros no es válido. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_payment_date (/es/errors/invalid_payment_date) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ---------------------------------- | | `invalid_payment_date` | `invalid_request_error` | 422 | [Pagos](/es/errors/index-payments) | ## Causa [#causa] La fecha de pago queda fuera de la ventana admitida: no puede ser anterior a la fecha de emisión de la factura ni situarse en el futuro. ## Qué hacer [#qué-hacer] Envía una fecha entre la de emisión y hoy, ambas incluidas. ## Relacionado [#relacionado] * [Todos los códigos de error de Pagos](/es/errors/index-payments) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_payment_method (/es/errors/invalid_payment_method) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `invalid_payment_method` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El método de pago queda fuera de la allowlist cerrada: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. ## Qué hacer [#qué-hacer] Envía uno de esos siete valores; la lista es cerrada y no se amplía por empresa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_period (/es/errors/invalid_period) | Code | Type | HTTP | Categoría | | ---------------- | ----------------------- | ---- | ------------------------------------------------- | | `invalid_period` | `invalid_request_error` | 422 | [Informes fiscales](/es/errors/index-tax-reports) | ## Causa [#causa] El período no identifica una declaración: el año queda fuera del rango admitido, o falta el trimestre o está fuera del rango 1 a 4 en un modelo trimestral. ## Qué hacer [#qué-hacer] Envía un año válido y, para el Modelo 303 y el Modelo 130, el trimestre de la declaración; el Modelo 347 es anual y no lleva trimestre. ## Relacionado [#relacionado] * [Todos los códigos de error de Informes fiscales](/es/errors/index-tax-reports) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_proforma_id (/es/errors/invalid_proforma_id) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_id` | `invalid_request_error` | 400 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La referencia de proforma recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. ## Qué hacer [#qué-hacer] Envía el `id` que devuelve la API para la proforma. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_proforma_number (/es/errors/invalid_proforma_number) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_number` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El número de proforma no sigue el formato canónico de numeración de su serie. ## Qué hacer [#qué-hacer] Envía el número tal cual aparece en el documento, con el prefijo de serie y el año. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_proforma_status (/es/errors/invalid_proforma_status) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_status` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El valor enviado como estado queda fuera del catálogo `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. ## Qué hacer [#qué-hacer] Usa uno de los valores del catálogo, escrito como lo devuelve la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_proforma_uuid (/es/errors/invalid_proforma_uuid) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ----------------------------------------------- | | `invalid_proforma_uuid` | `invalid_request_error` | 400 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El identificador de proforma de la ruta o del payload no es un UUID válido. ## Qué hacer [#qué-hacer] Copia el `id` exactamente como lo devolvió la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_purchase_invoice_id (/es/errors/invalid_purchase_invoice_id) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `invalid_purchase_invoice_id` | `invalid_request_error` | 400 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] La referencia de factura de compra recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. ## Qué hacer [#qué-hacer] Envía el `id` que devuelve la API para la factura de compra. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_purchase_invoice_number (/es/errors/invalid_purchase_invoice_number) | Code | Type | HTTP | Categoría | | --------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `invalid_purchase_invoice_number` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El número de factura está vacío o no encaja con el formato admitido. En una factura de compra el número es el que imprimió el proveedor, no uno que genere Factuarea. ## Qué hacer [#qué-hacer] Copia el número del documento del proveedor tal cual aparece allí. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_purchase_invoice_uuid (/es/errors/invalid_purchase_invoice_uuid) | Code | Type | HTTP | Categoría | | ------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `invalid_purchase_invoice_uuid` | `invalid_request_error` | 400 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El identificador de factura de compra de la ruta o del payload no es un UUID válido. ## Qué hacer [#qué-hacer] Copia el `id` exactamente como lo devolvió la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_rate_for_tax_regime (/es/errors/invalid_rate_for_tax_regime) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_rate_for_tax_regime` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El tipo no pertenece a la rejilla legal de su régimen: el IGIC admite 0, 3, 5, 7, 9,5, 15 y 20 %; el IPSI admite 0, 0,5, 1, 2, 4, 8 y 10 %. ## Qué hacer [#qué-hacer] Elige un tipo de la rejilla del régimen; si querías un tipo de IVA, comprueba que la zona AEAT del impuesto sea `peninsula`. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_recurring_invoice_id (/es/errors/invalid_recurring_invoice_id) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ----------------------------------------------------------- | | `invalid_recurring_invoice_id` | `invalid_request_error` | 400 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La referencia de recurrencia recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. ## Qué hacer [#qué-hacer] Envía el `id` que devuelve la API para la recurrencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_recurring_invoice_uuid (/es/errors/invalid_recurring_invoice_uuid) | Code | Type | HTTP | Categoría | | -------------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `invalid_recurring_invoice_uuid` | `invalid_request_error` | 400 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] El identificador de recurrencia de la ruta o del payload no es un UUID válido. ## Qué hacer [#qué-hacer] Copia el `id` exactamente como lo devolvió la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_series_code (/es/errors/invalid_series_code) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_code` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El código de la serie está vacío, es demasiado largo, o lleva caracteres que no corresponden a un prefijo fiscal. ## Qué hacer [#qué-hacer] Envía un prefijo alfanumérico corto; se guarda en mayúsculas y pasa a formar parte del número de todos los documentos de la serie. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_series_name (/es/errors/invalid_series_name) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_name` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El nombre de la serie está vacío o supera la longitud permitida. ## Qué hacer [#qué-hacer] Envía un nombre descriptivo y breve; el identificador fiscal es el código, no el nombre. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_series_number (/es/errors/invalid_series_number) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_number` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El número inicial no es válido: no es un entero positivo, o queda en el último número ya emitido o por debajo, lo que reemitiría números ya consumidos. ## Qué hacer [#qué-hacer] Envía un número inicial por encima del contador actual, u omítelo para continuar la secuencia natural. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_series_uuid (/es/errors/invalid_series_uuid) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_uuid` | `invalid_request_error` | 400 | [Series](/es/errors/index-series) | ## Causa [#causa] El identificador de serie de la ruta o del payload no es un UUID válido. ## Qué hacer [#qué-hacer] Copia el `id` exactamente como lo devolvió la API. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_series_year (/es/errors/invalid_series_year) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------- | | `invalid_series_year` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El ejercicio no es un año de cuatro cifras válido para una serie de numeración. ## Qué hacer [#qué-hacer] Envía el año con cuatro cifras, correspondiente al ejercicio que numera la serie. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_status_transition (/es/errors/invalid_status_transition) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_status_transition` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] El estado solicitado no es alcanzable desde el estado en el que está ahora mismo el documento. ## Qué hacer [#qué-hacer] Lee el `status` actual y recorre los pasos intermedios que exige el ciclo de vida del documento antes de pedir el estado destino. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_tax_code (/es/errors/invalid_tax_code) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | ----------------------------------- | | `invalid_tax_code` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El código del impuesto está vacío o supera los 50 caracteres. ## Qué hacer [#qué-hacer] Envía un código no vacío de hasta 50 caracteres que identifique al impuesto dentro de tu catálogo. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_tax_name (/es/errors/invalid_tax_name) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | ----------------------------------- | | `invalid_tax_name` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El nombre del impuesto está vacío o supera los 255 caracteres. ## Qué hacer [#qué-hacer] Envía un nombre no vacío de hasta 255 caracteres. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_tax_rate (/es/errors/invalid_tax_rate) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | ----------------------------------- | | `invalid_tax_rate` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El tipo impositivo queda fuera del rango permitido para su clase: IVA 0-27 %, retención 0-47 %, recargo de equivalencia 0-10 %, otros 0-100 %. ## Qué hacer [#qué-hacer] Envía un tipo dentro del rango de su clase, expresado como porcentaje y no como fracción (`21`, no `0.21`). ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_tax_type_filter (/es/errors/invalid_tax_type_filter) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_tax_type_filter` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El filtro `type` del listado por tipo lleva un valor fuera del enum `vat`, `retention`, `surcharge`, `other`. ## Qué hacer [#qué-hacer] Envía uno de los cuatro tipos, o quita el filtro para listar el catálogo completo. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invalid_validity_window (/es/errors/invalid_validity_window) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `invalid_validity_window` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La ventana de vigencia está invertida: `valid_until` es anterior a `valid_from`. ## Qué hacer [#qué-hacer] Envía `valid_until` igual o posterior a `valid_from`, u omítelo si el impuesto no tiene fecha de fin. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_already_annulled (/es/errors/invoice_already_annulled) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_already_annulled` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La factura ya estaba anulada. La anulación es terminal y, con VeriFactu activo, su registro de anulación ya llegó a la AEAT. ## Qué hacer [#qué-hacer] No repitas la anulación; si hay que volver a facturar la operación, emite una factura nueva. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_already_paid (/es/errors/invoice_already_paid) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_already_paid` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La factura ya está cobrada. `paid` es un estado terminal y contablemente cerrado: el IVA repercutido ya se ha declarado, o se declarará en el período. ## Qué hacer [#qué-hacer] Corrige una factura pagada emitiendo una rectificativa que la referencie; ya no admite edición ni anulación. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_already_sent (/es/errors/invoice_already_sent) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_already_sent` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La factura ya fue emitida: tiene número definitivo de serie y, con VeriFactu activo, su alta en la AEAT. La emisión no ocurre dos veces. ## Qué hacer [#qué-hacer] Sáltate el paso de emisión; para volver a entregarla usa la operación de envío, y para cambiar su contenido emite una rectificativa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_cannot_assign_number (/es/errors/invoice_cannot_assign_number) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_cannot_assign_number` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Se pidió número definitivo para una factura que no es borrador, o que ya lo tiene. La numeración de serie es monótona y los números no se reasignan. ## Qué hacer [#qué-hacer] Pide número solo sobre un borrador que aún muestre el placeholder; si la factura ya lo tiene, léelo del campo `number`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_invalid_status_transition (/es/errors/invoice_invalid_status_transition) | Code | Type | HTTP | Categoría | | ----------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_invalid_status_transition` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El estado destino no es alcanzable desde el actual. El ciclo de vida es dirigido: `draft` pasa a `scheduled` o `sent`, `sent` a `paid`, `overdue` o `annulled`, y `paid`, `cancelled` y `annulled` son terminales. ## Qué hacer [#qué-hacer] Lee el `status` actual y llama a la operación del paso que necesitas, en vez de fijar el estado destino directamente. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_cancellable_in_current_state (/es/errors/invoice_not_cancellable_in_current_state) | Code | Type | HTTP | Categoría | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_cancellable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Cancelar retira un borrador que todavía no es fiscalmente vinculante, así que solo aplica mientras la factura está en `draft`. ## Qué hacer [#qué-hacer] Si la factura ya está emitida, anúlala; si está pagada, corrígela con una rectificativa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_correctable_in_current_state (/es/errors/invoice_not_correctable_in_current_state) | Code | Type | HTTP | Categoría | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_correctable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Una rectificativa solo se emite contra una factura ya emitida (`sent` o `paid`). Un borrador, una factura cancelada o una anulada no tienen nada que rectificar. ## Qué hacer [#qué-hacer] Emite antes la factura original; mientras siga en borrador, edítala directamente en vez de rectificarla. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_deletable_in_current_state (/es/errors/invoice_not_deletable_in_current_state) | Code | Type | HTTP | Categoría | | ---------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Solo se borran las facturas en `draft` y `cancelled`. Una factura numerada nunca desaparece: la serie correlativa debe seguir siendo auditable. ## Qué hacer [#qué-hacer] Cancela el borrador, o anula la factura emitida; el borrado no es una vía para ella. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_editable_in_current_state (/es/errors/invoice_not_editable_in_current_state) | Code | Type | HTTP | Categoría | | --------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_editable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Solo un borrador admite edición. Una vez emitida, la factura es inmutable y su contenido queda congelado junto con su registro fiscal. ## Qué hacer [#qué-hacer] Emite una rectificativa con los importes correctos en lugar de editar esta factura. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_eligible_for_action (/es/errors/invoice_not_eligible_for_action) | Code | Type | HTTP | Categoría | | --------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_eligible_for_action` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La acción solicitada no aplica a esta factura: su tipo o su estado actual la dejan fuera del alcance de la operación. ## Qué hacer [#qué-hacer] Lee `status` y `type` de la factura y llama a la operación que les corresponde; la referencia indica qué estados admite cada acción. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_found (/es/errors/invoice_not_found) | Code | Type | HTTP | Categoría | | ------------------- | ----------------- | ---- | ------------------------------------- | | `invoice_not_found` | `not_found_error` | 404 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El identificador no resuelve a ninguna factura de la empresa autenticada. Las facturas de otra empresa responden exactamente igual. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo; si solo tienes tu propia referencia, localiza la factura por `external_id` o por número de factura. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_modifiable_in_current_state (/es/errors/invoice_not_modifiable_in_current_state) | Code | Type | HTTP | Categoría | | ----------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_modifiable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El campo que intentas cambiar está congelado para el estado actual — por ejemplo el régimen fiscal de una factura anulada. ## Qué hacer [#qué-hacer] Lee `error.message` para saber qué campo está implicado; en facturas emitidas, los cambios van por una rectificativa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_paid (/es/errors/invoice_not_paid) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_paid` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Se pidió un justificante de pago de una factura sin cobro registrado, así que no hay nada que certificar. ## Qué hacer [#qué-hacer] Registra antes el cobro y pide después el justificante. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_reschedulable_in_current_state (/es/errors/invoice_not_reschedulable_in_current_state) | Code | Type | HTTP | Categoría | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_reschedulable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Reprogramar mueve la fecha de emisión de una factura que está esperando en `scheduled`, y esta factura no está esperando. ## Qué hacer [#qué-hacer] Comprueba el `status`: si es `draft`, prográmala; si ya es `sent`, la emisión ocurrió y la fecha no se puede mover. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_schedulable_in_current_state (/es/errors/invoice_not_schedulable_in_current_state) | Code | Type | HTTP | Categoría | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_not_schedulable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Solo un borrador se puede programar: la programación reserva un momento futuro de emisión sin consumir todavía número de serie. ## Qué hacer [#qué-hacer] Programa la factura mientras siga en borrador; si ya está emitida, no queda nada que programar. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_unschedulable_in_current_state (/es/errors/invoice_not_unschedulable_in_current_state) | Code | Type | HTTP | Categoría | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_unschedulable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Desprogramar devuelve la factura de `scheduled` a `draft`, así que solo aplica mientras sigue esperando a emitirse. ## Qué hacer [#qué-hacer] Si la emisión programada ya se ejecutó, la factura está `sent`: deshazla anulándola o emitiendo una rectificativa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_not_unsendable_in_current_state (/es/errors/invoice_not_unsendable_in_current_state) | Code | Type | HTTP | Categoría | | ----------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_not_unsendable_in_current_state` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Deshacer la marca de entrega solo aplica a una factura `sent`: limpia `sent_at` y mantiene la factura emitida. ## Qué hacer [#qué-hacer] No lo uses sobre facturas pagadas, vencidas, anuladas o canceladas — esas piden una rectificativa o una anulación, no un deshacer. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_requires_at_least_one_line (/es/errors/invoice_requires_at_least_one_line) | Code | Type | HTTP | Categoría | | ------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La factura no lleva ninguna línea de operación, así que no tiene base imponible y no se puede emitir. Ocurre cuando no envías líneas y cuando todas las que envías son de suplido: un suplido es una cantidad pagada por cuenta del cliente (art. 78.Tres.3 LIVA), no una operación tuya. ## Qué hacer [#qué-hacer] Añade al menos una línea de operación (`line_type` NORMAL, el valor por defecto) con descripción, cantidad y precio unitario. Si lo que quieres es facturar el gasto como tuyo, repercútelo en una línea normal con su tipo de IVA en vez de declararlo suplido. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # invoice_year_required_for_ambiguous_number (/es/errors/invoice_year_required_for_ambiguous_number) | Code | Type | HTTP | Categoría | | -------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `invoice_year_required_for_ambiguous_number` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Ese número de factura existe en más de un ejercicio, así que por sí solo no identifica una única factura. ## Qué hacer [#qué-hacer] Repite la búsqueda añadiendo `year`; `error.message` enumera los años en los que existe ese número. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # ip_not_allowed (/es/errors/ip_not_allowed) | Code | Type | HTTP | Categoría | | ---------------- | ---------------------- | ---- | ------------------------------------------------ | | `ip_not_allowed` | `authentication_error` | 401 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] La clave restringe las direcciones que acepta, y la petición llegó desde una que no está en esa lista. ## Qué hacer [#qué-hacer] Añade la dirección de salida de tu servidor a la lista de la clave, o usa una clave sin restricción de IP para clientes cuya dirección cambia. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # length_required (/es/errors/length_required) | Code | Type | HTTP | Categoría | | ----------------- | ----------------------- | ---- | ----------------------------------- | | `length_required` | `invalid_request_error` | 411 | [Request](/es/errors/index-request) | ## Causa [#causa] Llegó una petición con body en codificación chunked, sin declarar su tamaño. La API necesita conocer la longitud por adelantado para rechazar payloads excesivos antes de cargarlos en memoria. ## Qué hacer [#qué-hacer] Envía el body con cabecera `Content-Length` en lugar de `Transfer-Encoding: chunked`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # line_total_checksum_mismatch (/es/errors/line_total_checksum_mismatch) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `line_total_checksum_mismatch` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El `line_total` declarado no coincide con el que calcula Factuarea para esa línea (cantidad × precio − descuento + IVA − retención + recargo) y la desviación supera el céntimo de tolerancia. El importe que se factura y se declara a la AEAT es siempre el calculado aquí, así que la discrepancia significa que tu sistema y la factura emitida no cuadrarían. ## Qué hacer [#qué-hacer] Compara `error.details.expected` (nuestro total) con `error.details.received` (el tuyo) y corrige el redondeo en tu lado. El campo es un checksum opcional de entrada que nunca se persiste: también puedes omitirlo y tomar los importes de la respuesta. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # line_type_invalid (/es/errors/line_type_invalid) | Code | Type | HTTP | Categoría | | ------------------- | ----------------------- | ---- | ------------------------------------- | | `line_type_invalid` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El tipo de línea queda fuera del catálogo cerrado `NORMAL` / `SUPLIDO`. Una factura emitida sólo distingue dos naturalezas: lo que vendes tú, que forma base imponible y lleva IVA, y el suplido, que es dinero adelantado en nombre y por cuenta del cliente y por eso queda fuera de la base (art. 78.Tres.3 LIVA). ## Qué hacer [#qué-hacer] Envía `NORMAL` para lo que factures como propio y `SUPLIDO` sólo para las cantidades que pagas a un tercero por cuenta del cliente; `error.details.allowed_values` trae el catálogo exacto. Un gasto tuyo que repercutes no es un suplido: va como `NORMAL` con su tipo de IVA. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # maintenance (/es/errors/maintenance) | Code | Type | HTTP | Categoría | | ------------- | --------------------------- | ---- | ----------------------------------- | | `maintenance` | `service_unavailable_error` | 503 | [Servidor](/es/errors/index-server) | ## Causa [#causa] La plataforma está en ventana de mantenimiento y las escrituras se retienen a propósito. ## Qué hacer [#qué-hacer] Reintenta cuando termine la ventana; encola las escrituras en tu lado para que no se pierda nada entretanto. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # max_api_keys_exceeded (/es/errors/max_api_keys_exceeded) | Code | Type | HTTP | Categoría | | ----------------------- | --------------------- | ---- | ---------------------------------------------- | | `max_api_keys_exceeded` | `authorization_error` | 422 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] La empresa alcanzó el número de API keys que permite su plan. ## Qué hacer [#qué-hacer] Revoca las claves que ya no uses antes de emitir una nueva, o sube de plan si de verdad necesitas más claves simultáneas. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # max_retries_exceeded (/es/errors/max_retries_exceeded) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | --------------------------------------- | | `max_retries_exceeded` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El registro agotó el presupuesto de reintentos técnicos de reenvío del XML almacenado. Reintentar el mismo contenido volvería a fallar igual. ## Qué hacer [#qué-hacer] Lee el error de la AEAT, corrige el dato de origen y usa el flujo de subsanación: regenera el contenido y reinicia la ronda de reintentos. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # max_webhook_endpoints_exceeded (/es/errors/max_webhook_endpoints_exceeded) | Code | Type | HTTP | Categoría | | -------------------------------- | --------------------- | ---- | ---------------------------------------------- | | `max_webhook_endpoints_exceeded` | `authorization_error` | 422 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] La empresa alcanzó el número de endpoints de webhook que permite su nivel de add-on. ## Qué hacer [#qué-hacer] Elimina los endpoints que ya no escuchas, o pasa a un nivel con límite mayor; un mismo endpoint puede suscribirse a varios tipos de evento. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # metadata_too_many_keys (/es/errors/metadata_too_many_keys) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `metadata_too_many_keys` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] El objeto `metadata` supera el límite de 50 claves por recurso. ## Qué hacer [#qué-hacer] Reduce `metadata` a 50 claves o menos y guarda el resto en tu sistema, indexado por el `id` del recurso. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # metadata_value_too_long (/es/errors/metadata_value_too_long) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `metadata_value_too_long` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Un valor de `metadata` supera los 500 caracteres una vez serializado a texto. ## Qué hacer [#qué-hacer] Acorta ese valor por debajo de 500 caracteres, o guarda el contenido largo en tu sistema y deja solo una referencia en `metadata`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # method_not_allowed (/es/errors/method_not_allowed) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | ----------------------------------- | | `method_not_allowed` | `invalid_request_error` | 405 | [Request](/es/errors/index-request) | ## Causa [#causa] La ruta existe pero no acepta el verbo HTTP utilizado. ## Qué hacer [#qué-hacer] Comprueba el verbo en la referencia del endpoint; la cabecera `Allow` de la respuesta lista los que admite esa ruta. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Método HTTP no permitido para esta ruta. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # missing_api_key (/es/errors/missing_api_key) | Code | Type | HTTP | Categoría | | ----------------- | ---------------------- | ---- | ------------------------------------------------ | | `missing_api_key` | `authentication_error` | 401 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] La petición no lleva credenciales: ni cabecera `Authorization` ni `X-API-Key`. ## Qué hacer [#qué-hacer] Envía `Authorization: Bearer <tu clave>`; la clave viaja en la cabecera, nunca en la query. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Falta header Authorization o X-API-Key. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # missing_required_param (/es/errors/missing_required_param) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `missing_required_param` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Un form request legacy detectó que faltaba un campo obligatorio. Los endpoints ya migrados a los parsers canónicos reportan lo mismo como `parameter_missing`. ## Qué hacer [#qué-hacer] Añade el campo que falta; si ramificas por código de error, trata este como alias de `parameter_missing`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # mode_switch_blocked_until_year_end (/es/errors/mode_switch_blocked_until_year_end) | Code | Type | HTTP | Categoría | | ------------------------------------ | ----------------------- | ---- | --------------------------------------- | | `mode_switch_blocked_until_year_end` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El modo VeriFactu se activó en este ejercicio y ya se emitió al menos un registro de facturación. Dar marcha atrás degradaría la integridad de una cadena ya declarada a la AEAT. ## Qué hacer [#qué-hacer] Espera al 31 de diciembre del año en curso; la vuelta atrás solo está disponible mientras la empresa no ha emitido su primer registro. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # module_not_available_in_sandbox (/es/errors/module_not_available_in_sandbox) | Code | Type | HTTP | Categoría | | --------------------------------- | --------------------- | ---- | ---------------------------------------------- | | `module_not_available_in_sandbox` | `authorization_error` | 403 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] El recurso pertenece a un módulo vetado en modo test. La sandbox nunca toca AEAT, bancos ni cobros reales, así que esos módulos quedan fuera a propósito. ## Qué hacer [#qué-hacer] Prueba la operación con una clave de producción sobre una empresa real; esta restricción es del entorno, no del plan. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # monthly_quota_exceeded (/es/errors/monthly_quota_exceeded) | Code | Type | HTTP | Categoría | | ------------------------ | ------------------ | ---- | --------------------------------------------- | | `monthly_quota_exceeded` | `rate_limit_error` | 429 | [Límite de tasa](/es/errors/index-rate-limit) | ## Causa [#causa] La empresa agotó la cuota mensual de llamadas que incluye su plan. ## Qué hacer [#qué-hacer] Espera al siguiente ciclo de facturación o sube de plan; mientras tanto, reduce el sondeo suscribiéndote a webhooks en lugar de releer colecciones. ## Relacionado [#relacionado] * [Todos los códigos de error de Límite de tasa](/es/errors/index-rate-limit) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # monthly_requires_month_segmented_format (/es/errors/monthly_requires_month_segmented_format) | Code | Type | HTTP | Categoría | | ----------------------------------------- | ----------------------- | ---- | --------------------------------- | | `monthly_requires_month_segmented_format` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El contador se reinicia cada mes pero la máscara de numeración no segrega por mes, así que dos meses arrancarían en el mismo correlativo y producirían números duplicados dentro del año. ## Qué hacer [#qué-hacer] Añade el token de mes a `number_format`, o cambia la política de reinicio a anual o a nunca. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # no_invoices_in_period (/es/errors/no_invoices_in_period) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ------------------------------------- | | `no_invoices_in_period` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La operación trimestral no encontró facturas en el período pedido, así que no hay nada que empaquetar ni enviar. ## Qué hacer [#qué-hacer] Revisa el año y el trimestre y elige un período con facturas emitidas. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # notification_not_found (/es/errors/notification_not_found) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------- | ---- | ------------------------------------------------ | | `notification_not_found` | `not_found_error` | 404 | [Notificaciones](/es/errors/index-notifications) | ## Causa [#causa] El identificador no corresponde a ninguna notificación de la empresa autenticada, o la notificación quedó fuera de la ventana de retención. ## Qué hacer [#qué-hacer] Lista las notificaciones para obtener un `id` vigente. ## Relacionado [#relacionado] * [Todos los códigos de error de Notificaciones](/es/errors/index-notifications) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # operation_regime_invalid (/es/errors/operation_regime_invalid) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `operation_regime_invalid` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El régimen de operación queda fuera del catálogo `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. ## Qué hacer [#qué-hacer] Elige el régimen que corresponde a la operación: decide cómo se declara el IVA y si aplica la inversión del sujeto pasivo. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # origin_not_allowed (/es/errors/origin_not_allowed) | Code | Type | HTTP | Categoría | | -------------------- | ---------------------- | ---- | ------------------------------------------------ | | `origin_not_allowed` | `authentication_error` | 401 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] La petición viene de un origen de navegador que la clave no acepta. ## Qué hacer [#qué-hacer] Añade el origen a la configuración de la clave, o mueve la llamada a tu servidor: una API key nunca debe quedar expuesta en un navegador. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # pack_in_use (/es/errors/pack_in_use) | Code | Type | HTTP | Categoría | | ------------- | ----------------------- | ---- | -------------------------------------- | | `pack_in_use` | `invalid_request_error` | 422 | [Productos](/es/errors/index-products) | ## Causa [#causa] El pack está referenciado por documentos emitidos, así que borrarlo rompería su composición. ## Qué hacer [#qué-hacer] Desactiva el pack en lugar de borrarlo, o quita el producto del pack si era eso lo que querías cambiar. ## Relacionado [#relacionado] * [Todos los códigos de error de Productos](/es/errors/index-products) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # pack_not_found (/es/errors/pack_not_found) | Code | Type | HTTP | Categoría | | ---------------- | ----------------- | ---- | -------------------------------------- | | `pack_not_found` | `not_found_error` | 404 | [Productos](/es/errors/index-products) | ## Causa [#causa] El identificador no resuelve a ningún pack de la empresa autenticada. ## Qué hacer [#qué-hacer] Lista los packs y usa el `id` que devuelven. ## Relacionado [#relacionado] * [Todos los códigos de error de Productos](/es/errors/index-products) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # pack_share_link_failed (/es/errors/pack_share_link_failed) | Code | Type | HTTP | Categoría | | ------------------------ | ----------- | ---- | -------------------------------------- | | `pack_share_link_failed` | `api_error` | 500 | [Productos](/es/errors/index-products) | ## Causa [#causa] No se pudo generar el enlace para compartir el pack. El pack en sí no queda afectado. ## Qué hacer [#qué-hacer] Reintenta pasados unos segundos y comunica el `request_id` si sigue fallando. ## Relacionado [#relacionado] * [Todos los códigos de error de Productos](/es/errors/index-products) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid (/es/errors/parameter_invalid) | Code | Type | HTTP | Categoría | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Un value object construido a partir del payload rechazó el valor recibido. `error.subcode` dice cuál: código de impuesto, código de país, tipo impositivo, etc. ## Qué hacer [#qué-hacer] Corrige el campo que indica `error.param` siguiendo el formato del concepto que nombra `error.subcode`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_boolean (/es/errors/parameter_invalid_boolean) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_boolean` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un parámetro que debe ser booleano recibió un valor fuera de las representaciones aceptadas (`true`/`false`, `1`/`0`). ## Qué hacer [#qué-hacer] Envía `true` o `false` en el parámetro que indica `error.param`. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El parámetro debe ser un valor booleano. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_cursor (/es/errors/parameter_invalid_cursor) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_cursor` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] El cursor `starting_after` o `ending_before` no es un UUID válido, así que no puede apuntar a ninguna fila de la colección. ## Qué hacer [#qué-hacer] Usa como cursor el `id` del último objeto de la página anterior (o del primero, para `ending_before`), copiado literalmente de la respuesta. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_empty (/es/errors/parameter_invalid_empty) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_empty` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un parámetro llegó con el valor vacío: un filtro `in` sin elementos, una comparación sin nada tras el operador, o un filtro de igualdad con la cadena vacía. ## Qué hacer [#qué-hacer] Envía un valor no vacío en el parámetro de `error.param`, o quita el parámetro de la petición. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_enum (/es/errors/parameter_invalid_enum) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_enum` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] El valor queda fuera del conjunto cerrado que acepta el parámetro. En los listados cubre además un operador de filtro distinto de `eq`, `gte`, `lte`, `gt`, `lt`, `in` o `contains`. ## Qué hacer [#qué-hacer] Elige uno de los valores documentados para ese parámetro, o uno de los operadores de filtro admitidos. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El valor del parámetro no está entre los permitidos. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_format (/es/errors/parameter_invalid_format) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_format` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] El valor tiene el tipo correcto pero no la forma que exige el parámetro: una fecha, un patrón de identificador o una cabecera como `Factuarea-Version`. ## Qué hacer [#qué-hacer] Reescribe el valor de `error.param` con el patrón documentado para ese campo y repite la llamada. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El formato del parámetro no es válido. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_integer (/es/errors/parameter_invalid_integer) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_integer` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un parámetro que debe ser un número entero recibió algo que no se puede interpretar como tal, por ejemplo `limit=abc`. ## Qué hacer [#qué-hacer] Envía el parámetro de `error.param` como entero en base 10, sin decimales, separadores de millares ni comillas. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El parámetro debe ser un número entero. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_iso8601 (/es/errors/parameter_invalid_iso8601) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_iso8601` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un filtro de rango (`gte`, `lte`, `gt`, `lt`) recibió un valor que no es numérico ni una fecha ISO 8601. ## Qué hacer [#qué-hacer] Envía las fechas como `YYYY-MM-DD`, o en ISO 8601 completo con zona horaria (`2026-01-31T23:59:59Z`). ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_range (/es/errors/parameter_invalid_range) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_range` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un parámetro numérico quedó fuera de sus límites. El caso habitual es `limit`, que debe estar entre 1 y 100. ## Qué hacer [#qué-hacer] Envía un valor dentro de los límites documentados; para leer más de 100 objetos, pagina con `starting_after`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_string (/es/errors/parameter_invalid_string) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_string` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un parámetro que debe ser texto recibió un array, un objeto o un valor que no se puede leer como cadena. ## Qué hacer [#qué-hacer] Envía el parámetro de `error.param` como una cadena de texto simple. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El parámetro debe ser una cadena de texto. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_url (/es/errors/parameter_invalid_url) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_url` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un campo que debe contener una URL absoluta recibió un valor que no lo es, normalmente por faltarle el esquema o el host. ## Qué hacer [#qué-hacer] Envía una URL absoluta `https://` en el campo que indica `error.param`. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El parámetro debe ser una URL válida. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_uuid (/es/errors/parameter_invalid_uuid) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_uuid` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] Un campo de identificador recibió un valor que no es un UUID válido. Todo `id` de recurso en v1 es un UUID. ## Qué hacer [#qué-hacer] Usa el `id` que devolvió la API para ese recurso, copiado literalmente; nunca un id numérico interno ni un valor truncado. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El parámetro debe ser un UUID válido. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_invalid_value (/es/errors/parameter_invalid_value) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_invalid_value` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] El valor es sintácticamente correcto pero no admisible para este recurso: fuera del catálogo canónico del campo, o incoherente con el resto del payload. ## Qué hacer [#qué-hacer] Lee `error.param` y `error.subcode`: entre ambos identifican el campo y la regla concreta que incumple el valor. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El valor del parámetro no es válido. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_missing (/es/errors/parameter_missing) | Code | Type | HTTP | Categoría | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_missing` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] El endpoint exige un parámetro que la petición no llevaba. `error.param` dice cuál. ## Qué hacer [#qué-hacer] Añade el parámetro indicado en `error.param` a la query o al body y repite la llamada. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Falta un parámetro requerido. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # parameter_unknown (/es/errors/parameter_unknown) | Code | Type | HTTP | Categoría | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `parameter_unknown` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] La petición lleva un parámetro que el endpoint no acepta: un filtro fuera de su allowlist, un campo de `sort` no ordenable, o el `page` de paginación por offset — v1 pagina por cursor. ## Qué hacer [#qué-hacer] Quita el parámetro que indica `error.param`; para recorrer una colección usa `limit` junto con `starting_after` o `ending_before`. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # payload_too_large (/es/errors/payload_too_large) | Code | Type | HTTP | Categoría | | ------------------- | ----------------------- | ---- | ----------------------------------- | | `payload_too_large` | `invalid_request_error` | 413 | [Request](/es/errors/index-request) | ## Causa [#causa] El body de la petición supera el tamaño admitido: 1 MB con carácter general, 6 MB en los endpoints que aceptan ficheros. ## Qué hacer [#qué-hacer] Parte la operación en peticiones más pequeñas, o comprime el adjunto antes de enviarlo. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El payload excede el límite de 1 MB. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # payment_method_invalid (/es/errors/payment_method_invalid) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ------------------------------------- | | `payment_method_invalid` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La misma allowlist cerrada que `invalid_payment_method`, reportada cuando el valor se rechaza al leer el campo de método de pago del payload. ## Qué hacer [#qué-hacer] Envía uno de los siete métodos admitidos, en minúsculas y con guion bajo, como `sepa_direct_debit`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # payment_method_required (/es/errors/payment_method_required) | Code | Type | HTTP | Categoría | | ------------------------- | ------------------------ | ---- | -------------------------------------- | | `payment_method_required` | `payment_required_error` | 402 | [Empresas](/es/errors/index-companies) | ## Causa [#causa] Dar de alta una empresa gestionada cobra un asiento de inmediato, y la gestoría opera en modo real sin método de pago configurado. ## Qué hacer [#qué-hacer] Abre el portal de facturación en `error.details.payment_setup_url`, registra un método de pago y repite la misma llamada. ## Relacionado [#relacionado] * [Todos los códigos de error de Empresas](/es/errors/index-companies) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # payout_reconciliation_amount_mismatch (/es/errors/payout_reconciliation_amount_mismatch) | Code | Type | HTTP | Categoría | | --------------------------------------- | ----------------------- | ---- | ---------------------------------- | | `payout_reconciliation_amount_mismatch` | `invalid_request_error` | 422 | [Pagos](/es/errors/index-payments) | ## Causa [#causa] El importe confirmado no coincide con el neto de la liquidación, así que la conciliación cerraría con una diferencia que nadie justifica. ## Qué hacer [#qué-hacer] Concilia contra el importe neto —bruto menos comisiones de Stripe— y comprueba que el movimiento bancario corresponde a esta liquidación. ## Relacionado [#relacionado] * [Todos los códigos de error de Pagos](/es/errors/index-payments) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # pdf_generation_failed (/es/errors/pdf_generation_failed) | Code | Type | HTTP | Categoría | | ----------------------- | --------------------------- | ---- | ----------------------------------- | | `pdf_generation_failed` | `service_unavailable_error` | 503 | [Servidor](/es/errors/index-server) | ## Causa [#causa] El servicio de renderizado no pudo producir el PDF. El documento y sus datos están intactos: lo que falló es el fichero. ## Qué hacer [#qué-hacer] Reintenta pasados unos segundos; si persiste, comunica el `request_id` a soporte y comparte mientras tanto el documento por su enlace público. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # product_in_use (/es/errors/product_in_use) | Code | Type | HTTP | Categoría | | ---------------- | ----------------------- | ---- | -------------------------------------- | | `product_in_use` | `invalid_request_error` | 422 | [Productos](/es/errors/index-products) | ## Causa [#causa] El producto está referenciado por documentos emitidos o por otras entradas del catálogo, y eliminarlo dejaría esas referencias colgando. ## Qué hacer [#qué-hacer] Desactiva el producto en lugar de borrarlo: deja de ofrecerse y los documentos que lo usaron siguen intactos. ## Relacionado [#relacionado] * [Todos los códigos de error de Productos](/es/errors/index-products) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # product_not_found (/es/errors/product_not_found) | Code | Type | HTTP | Categoría | | ------------------- | ----------------- | ---- | -------------------------------------- | | `product_not_found` | `not_found_error` | 404 | [Productos](/es/errors/index-products) | ## Causa [#causa] El identificador no resuelve a ningún producto de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id`, o busca el producto por su SKU o su `external_id` antes de crear un duplicado. ## Relacionado [#relacionado] * [Todos los códigos de error de Productos](/es/errors/index-products) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # profile_not_found (/es/errors/profile_not_found) | Code | Type | HTTP | Categoría | | ------------------- | ----------------- | ---- | ----------------------------------- | | `profile_not_found` | `not_found_error` | 404 | [Request](/es/errors/index-request) | ## Causa [#causa] La cabecera `X-Active-Profile` nombra una empresa que no existe o que no pertenece al árbol de gestoría de la clave autenticada. Ambos casos responden igual para que la API nunca revele empresas de otros tenants. ## Qué hacer [#qué-hacer] Envía el `id` de una de las empresas gestionadas que lista `GET /v1/companies`, o quita la cabecera para operar sobre tu propia empresa. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_already_accepted (/es/errors/proforma_already_accepted) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_already_accepted` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El cliente ya aceptó la proforma, y la aceptación se registra una sola vez. ## Qué hacer [#qué-hacer] Pasa a la conversión en factura; no queda nada que aceptar. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_already_rejected (/es/errors/proforma_already_rejected) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_already_rejected` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La proforma ya está marcada como rechazada. ## Qué hacer [#qué-hacer] Si el cliente cambió de opinión, registra la aceptación: una proforma rechazada todavía se puede aceptar. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_cannot_be_accepted (/es/errors/proforma_cannot_be_accepted) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_cannot_be_accepted` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La aceptación no procede desde el estado actual: una proforma facturada, cancelada o expirada ya no la admite. ## Qué hacer [#qué-hacer] Emite una proforma nueva con las condiciones vigentes y haz que se acepte esa. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_cannot_be_rejected (/es/errors/proforma_cannot_be_rejected) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_cannot_be_rejected` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El rechazo no procede desde el estado actual: una vez facturada, cancelada o expirada, la proforma está cerrada. ## Qué hacer [#qué-hacer] Si la operación no sigue adelante y la proforma ya se facturó, corrige la factura en lugar de rechazar la proforma. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_cannot_be_sent (/es/errors/proforma_cannot_be_sent) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_cannot_be_sent` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El envío por email no aplica a una proforma en estado terminal: no hay oferta viva que entregar. ## Qué hacer [#qué-hacer] Emite una proforma nueva y envía esa; un documento cerrado solo se comparte como descarga. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_invalid_status_transition (/es/errors/proforma_invalid_status_transition) | Code | Type | HTTP | Categoría | | ------------------------------------ | ----------------------- | ---- | ----------------------------------------------- | | `proforma_invalid_status_transition` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El estado destino no es alcanzable desde el actual: un borrador se acepta, se cancela o expira; una proforma aceptada se factura, se rechaza o expira; facturada, cancelada y expirada son terminales. ## Qué hacer [#qué-hacer] Lee el `status` actual y recorre el paso intermedio que exige el ciclo de vida. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_not_convertible_in_current_state (/es/errors/proforma_not_convertible_in_current_state) | Code | Type | HTTP | Categoría | | ------------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_convertible_in_current_state` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] Convertir en factura exige que el cliente haya aceptado la proforma; desde cualquier otro estado no hay acuerdo que facturar. ## Qué hacer [#qué-hacer] Registra antes la aceptación y convierte después; si el cliente nunca la aceptó, emite la factura directamente. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_not_deletable_in_current_state (/es/errors/proforma_not_deletable_in_current_state) | Code | Type | HTTP | Categoría | | ----------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] Solo se borra una proforma en borrador. Una vez aceptada, rechazada o facturada forma parte del rastro comercial. ## Qué hacer [#qué-hacer] Cancela la proforma en lugar de borrarla: la cancelación conserva el histórico y la retira de circulación. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_not_draft (/es/errors/proforma_not_draft) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_draft` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La operación solo tiene sentido mientras la proforma es un borrador, y esta ya ha avanzado. ## Qué hacer [#qué-hacer] Lee el `status` y usa la operación que le corresponde, o parte de un borrador nuevo. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_not_editable_in_current_state (/es/errors/proforma_not_editable_in_current_state) | Code | Type | HTTP | Categoría | | ---------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_not_editable_in_current_state` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] Solo una proforma en borrador admite edición. Una vez aceptada, rechazada, expirada, facturada o cancelada, su contenido queda fijado. ## Qué hacer [#qué-hacer] Duplica la proforma para trabajar sobre un borrador nuevo, en lugar de editar la que ya está cerrada. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_not_found (/es/errors/proforma_not_found) | Code | Type | HTTP | Categoría | | -------------------- | ----------------- | ---- | ----------------------------------------------- | | `proforma_not_found` | `not_found_error` | 404 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] El identificador no resuelve a ninguna proforma de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo, o localiza la proforma por su `external_id`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # proforma_requires_at_least_one_line (/es/errors/proforma_requires_at_least_one_line) | Code | Type | HTTP | Categoría | | ------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `proforma_requires_at_least_one_line` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La proforma no lleva líneas, así que no hay importe que poner delante del cliente. ## Qué hacer [#qué-hacer] Añade al menos una línea con descripción, cantidad y precio unitario. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # public_link_expires_at_exceeds_max_days (/es/errors/public_link_expires_at_exceeds_max_days) | Code | Type | HTTP | Categoría | | ----------------------------------------- | ----------------------- | ---- | ----------------------------------------------- | | `public_link_expires_at_exceeds_max_days` | `invalid_request_error` | 422 | [Facturas proforma](/es/errors/index-proformas) | ## Causa [#causa] La caducidad pedida para el enlace público supera la ventana máxima que permite tu plan para documentos compartidos. ## Qué hacer [#qué-hacer] Envía un `expires_at` más cercano; cuando el enlace caduque puedes renovarlo tantas veces como necesites. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas proforma](/es/errors/index-proformas) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # purchase_invoice_already_exists (/es/errors/purchase_invoice_already_exists) | Code | Type | HTTP | Categoría | | --------------------------------- | ---------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_already_exists` | `conflict_error` | 409 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] Ese proveedor ya tiene registrada una factura de compra con el mismo número. El par proveedor + número identifica el documento sin ambigüedad y evita contabilizar dos veces el mismo gasto. ## Qué hacer [#qué-hacer] Actualiza la factura existente en lugar de volver a registrarla, o revisa el número si el proveedor emitió realmente dos documentos. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # purchase_invoice_not_deletable_in_current_state (/es/errors/purchase_invoice_not_deletable_in_current_state) | Code | Type | HTTP | Categoría | | ------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_deletable_in_current_state` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] Solo se borran las facturas de compra en borrador o canceladas. Una pendiente o pagada forma parte del libro de gastos. ## Qué hacer [#qué-hacer] Cancela la factura en lugar de borrarla; una vez cancelada sí se puede eliminar si de verdad no quieres dejarla registrada. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # purchase_invoice_not_draft (/es/errors/purchase_invoice_not_draft) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_draft` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] La operación solo aplica mientras la factura de compra es un borrador, y esta ya está registrada. ## Qué hacer [#qué-hacer] Lee el `status` y usa la operación que le corresponde: las facturas registradas cambian por pago o por cancelación, no por edición de borrador. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # purchase_invoice_not_editable_in_current_state (/es/errors/purchase_invoice_not_editable_in_current_state) | Code | Type | HTTP | Categoría | | ------------------------------------------------ | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_editable_in_current_state` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] Solo se edita una factura de compra en borrador. Una vez registrada como pendiente, pagada o cancelada, su contenido respalda un apunte contable. ## Qué hacer [#qué-hacer] Devuélvela a borrador si todavía está pendiente, o registra la diferencia con un documento nuevo si ya está liquidada. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # purchase_invoice_not_found (/es/errors/purchase_invoice_not_found) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_not_found` | `not_found_error` | 404 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] El identificador no resuelve a ninguna factura de compra de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo, o localiza la factura por su `external_id`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # purchase_invoice_requires_at_least_one_line (/es/errors/purchase_invoice_requires_at_least_one_line) | Code | Type | HTTP | Categoría | | --------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------- | | `purchase_invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Facturas de compra](/es/errors/index-purchase-invoices) | ## Causa [#causa] La factura de compra no lleva líneas, así que no hay gasto ni IVA soportado que registrar. ## Qué hacer [#qué-hacer] Añade al menos una línea con descripción, cantidad y precio unitario antes de guardar. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas de compra](/es/errors/index-purchase-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # quote_already_accepted (/es/errors/quote_already_accepted) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `quote_already_accepted` | `invalid_request_error` | 422 | [Presupuestos](/es/errors/index-quotes) | ## Causa [#causa] El presupuesto ya estaba aprobado, y la aprobación se registra una sola vez. ## Qué hacer [#qué-hacer] Pasa a la conversión en factura; no queda nada que aprobar. ## Relacionado [#relacionado] * [Todos los códigos de error de Presupuestos](/es/errors/index-quotes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # quote_already_rejected (/es/errors/quote_already_rejected) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `quote_already_rejected` | `invalid_request_error` | 422 | [Presupuestos](/es/errors/index-quotes) | ## Causa [#causa] El presupuesto ya está marcado como rechazado. ## Qué hacer [#qué-hacer] Si el cliente cambió de opinión, registra la aprobación: un presupuesto rechazado todavía se puede aprobar. ## Relacionado [#relacionado] * [Todos los códigos de error de Presupuestos](/es/errors/index-quotes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # quote_expired (/es/errors/quote_expired) | Code | Type | HTTP | Categoría | | --------------- | ----------------------- | ---- | --------------------------------------- | | `quote_expired` | `invalid_request_error` | 422 | [Presupuestos](/es/errors/index-quotes) | ## Causa [#causa] El presupuesto pasó su fecha de validez, así que las condiciones ofrecidas ya no vinculan y no se puede aprobar ni convertir tal cual. ## Qué hacer [#qué-hacer] Duplica el presupuesto con una fecha de validez nueva y haz que el cliente apruebe ese. ## Relacionado [#relacionado] * [Todos los códigos de error de Presupuestos](/es/errors/index-quotes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # quote_not_found (/es/errors/quote_not_found) | Code | Type | HTTP | Categoría | | ----------------- | ----------------- | ---- | --------------------------------------- | | `quote_not_found` | `not_found_error` | 404 | [Presupuestos](/es/errors/index-quotes) | ## Causa [#causa] El identificador no resuelve a ningún presupuesto de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo, o localiza el presupuesto por su `external_id`. ## Relacionado [#relacionado] * [Todos los códigos de error de Presupuestos](/es/errors/index-quotes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # rate_limit_exceeded (/es/errors/rate_limit_exceeded) | Code | Type | HTTP | Categoría | | --------------------- | ------------------ | ---- | --------------------------------------------- | | `rate_limit_exceeded` | `rate_limit_error` | 429 | [Límite de tasa](/es/errors/index-rate-limit) | ## Causa [#causa] La clave envió más peticiones de las que permite su ritmo en la ventana actual. ## Qué hacer [#qué-hacer] Lee la cabecera `Retry-After` y espera ese tiempo; reparte el trabajo masivo y usa los endpoints en lote en vez de una llamada por objeto. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Has excedido el límite de requests permitido. Reintenta más tarde. ## Relacionado [#relacionado] * [Todos los códigos de error de Límite de tasa](/es/errors/index-rate-limit) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # receipt_not_available (/es/errors/receipt_not_available) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ---------------------------------- | | `receipt_not_available` | `invalid_request_error` | 422 | [Pagos](/es/errors/index-payments) | ## Causa [#causa] No hay justificante que emitir porque el documento no tiene ningún cobro registrado detrás. ## Qué hacer [#qué-hacer] Registra antes el cobro; el justificante certifica un pago que ya existe. ## Relacionado [#relacionado] * [Todos los códigos de error de Pagos](/es/errors/index-payments) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # record_already_accepted (/es/errors/record_already_accepted) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | --------------------------------------- | | `record_already_accepted` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La AEAT ya aceptó el registro. La aceptación es terminal y su contenido queda congelado como parte de la cadena de huellas. ## Qué hacer [#qué-hacer] Para corregir una factura aceptada, emite una rectificativa: un registro aceptado no se reenvía ni se subsana. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # record_immutable (/es/errors/record_immutable) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | --------------------------------------- | | `record_immutable` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El registro pertenece a un ledger de solo-adición: una vez escrito, su contenido fiscal queda cerrado a modificaciones y a borrado. ## Qué hacer [#qué-hacer] Añade un registro nuevo que lo corrija — anulación más alta nueva, o rectificativa — en lugar de editar el existente. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # record_not_rejected (/es/errors/record_not_rejected) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------------- | | `record_not_rejected` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La subsanación solo aplica a registros que la AEAT rechazó por datos. Este registro está en otro estado — un fallo técnico, por ejemplo, lo cubre el reintento automático. ## Qué hacer [#qué-hacer] Reintenta la transmisión si el fallo fue técnico; si la AEAT aceptó el registro, corrige la factura con una rectificativa. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # record_not_subsanable (/es/errors/record_not_subsanable) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | --------------------------------------- | | `record_not_subsanable` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El registro no se puede subsanar: no es un registro de alta, o no tiene factura de origen desde la que regenerar su contenido. ## Qué hacer [#qué-hacer] Usa una anulación más un alta nueva, o emite una rectificativa, según lo que haya que cambiar. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_already_active (/es/errors/recurring_already_active) | Code | Type | HTTP | Categoría | | -------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_already_active` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia ya está en marcha, así que no hay nada que activar. Código legacy conservado por compatibilidad: los endpoints actuales reportan esto como `recurring_invoice_already_active`. ## Qué hacer [#qué-hacer] Lee `status` antes de actuar; para cambiar la programación, actualiza la recurrencia en vez de volver a activarla. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_already_active (/es/errors/recurring_invoice_already_active) | Code | Type | HTTP | Categoría | | ---------------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_already_active` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia ya está en marcha. ## Qué hacer [#qué-hacer] Lee `status` antes de actuar; para cambiar cuándo se ejecuta la próxima vez, actualiza la recurrencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_already_cancelled (/es/errors/recurring_invoice_already_cancelled) | Code | Type | HTTP | Categoría | | ------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_already_cancelled` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia ya estaba cancelada, y la cancelación es terminal. ## Qué hacer [#qué-hacer] Crea una recurrencia nueva si necesitas volver a facturar periódicamente a ese cliente. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_already_paused (/es/errors/recurring_invoice_already_paused) | Code | Type | HTTP | Categoría | | ---------------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_already_paused` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia ya está pausada, así que pausarla otra vez no cambia nada. ## Qué hacer [#qué-hacer] Lee `status` antes de actuar; para recuperarla usa la operación de reanudar. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_cancelled_cannot_resume (/es/errors/recurring_invoice_cancelled_cannot_resume) | Code | Type | HTTP | Categoría | | ------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_cancelled_cannot_resume` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] Una recurrencia cancelada no se reanuda: la cancelación la cierra definitivamente, a diferencia de la pausa. ## Qué hacer [#qué-hacer] Duplícala en una recurrencia nueva, o usa la pausa en vez de la cancelación cuando la parada sea temporal. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_cannot_run (/es/errors/recurring_invoice_cannot_run) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_cannot_run` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia no puede generar una factura ahora mismo: no está en marcha, su ciclo terminó, o le faltan datos que la factura necesita. `error.message` indica el motivo concreto. ## Qué hacer [#qué-hacer] Arregla lo que indica el mensaje —reanudarla, ampliar el número de ocurrencias o completar los datos que falten— antes de forzar una ejecución. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_has_generated_invoices (/es/errors/recurring_invoice_has_generated_invoices) | Code | Type | HTTP | Categoría | | ------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_has_generated_invoices` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia ya generó facturas, y esas facturas dependen de ella para su trazabilidad. ## Qué hacer [#qué-hacer] Cancela la recurrencia en lugar de borrarla: deja de generar y las facturas ya emitidas conservan su origen. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_not_found (/es/errors/recurring_invoice_not_found) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_not_found` | `not_found_error` | 404 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] El identificador no resuelve a ninguna recurrencia de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo, o localiza la recurrencia por su `external_id`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_invoice_requires_at_least_one_line (/es/errors/recurring_invoice_requires_at_least_one_line) | Code | Type | HTTP | Categoría | | ---------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_invoice_requires_at_least_one_line` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La recurrencia no lleva líneas, así que cada factura generada saldría vacía. ## Qué hacer [#qué-hacer] Añade al menos una línea con descripción, cantidad y precio unitario antes de guardar o activar la recurrencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # recurring_not_active (/es/errors/recurring_not_active) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ----------------------------------------------------------- | | `recurring_not_active` | `invalid_request_error` | 422 | [Facturas recurrentes](/es/errors/index-recurring-invoices) | ## Causa [#causa] La operación necesita una recurrencia en marcha y esta está pausada, completada o cancelada. Código legacy conservado por compatibilidad con integraciones antiguas. ## Qué hacer [#qué-hacer] Reanuda la recurrencia antes de la operación, o lee `status` para ver por qué se detuvo. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas recurrentes](/es/errors/index-recurring-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # register_sealing_failed (/es/errors/register_sealing_failed) | Code | Type | HTTP | Categoría | | ------------------------- | ----------- | ---- | ----------------------------------- | | `register_sealing_failed` | `api_error` | 500 | [Servidor](/es/errors/index-server) | ## Causa [#causa] El sellado criptográfico del registro no se completó, así que el cierre quedó sin firmar en lugar de sellado con una firma rota. ## Qué hacer [#qué-hacer] Revisa el certificado de firma de la empresa y repite el cierre; comunica el `request_id` si el fallo se repite con un certificado válido. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # reminder_not_applicable (/es/errors/reminder_not_applicable) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ------------------------------------- | | `reminder_not_applicable` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] El recordatorio de pago no procede: la factura no está en `sent` ni `overdue`, no hay email de destinatario, falta el enlace público o está desactivado, o ya salió otro recordatorio en las últimas 24 horas. ## Qué hacer [#qué-hacer] Revisa el estado, activa el enlace público, indica un email de destinatario y respeta la espera de 24 horas antes de reintentar. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # replay_delivery_not_retryable (/es/errors/replay_delivery_not_retryable) | Code | Type | HTTP | Categoría | | ------------------------------- | ----------------------- | ---- | ------------------------------------- | | `replay_delivery_not_retryable` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] Solo se reenvían las entregas fallidas. Una entrega que llegó bien, o una todavía en curso, no tiene nada que reenviar. ## Qué hacer [#qué-hacer] Lee el `status` de la entrega: el reenvío aplica a las fallidas; para una entrega correcta, vuelve a leer el evento en su lugar. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # replay_event_expired (/es/errors/replay_event_expired) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ------------------------------------- | | `replay_event_expired` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El evento que respalda la entrega fue purgado por la política de retención de 30 días, así que ya no queda payload que reenviar. ## Qué hacer [#qué-hacer] Reconstruye el estado desde el recurso afectado a través de su endpoint; los eventos de más de 30 días no se recuperan. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # report_format_invalid (/es/errors/report_format_invalid) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ------------------------------------------------- | | `report_format_invalid` | `invalid_request_error` | 422 | [Informes fiscales](/es/errors/index-tax-reports) | ## Causa [#causa] El formato queda fuera del catálogo `txt_aeat`, `pdf`, `excel`. ## Qué hacer [#qué-hacer] Envía `txt_aeat` para presentar ante la AEAT, `pdf` para una copia legible, o `excel` para trabajar sobre las cifras. ## Relacionado [#relacionado] * [Todos los códigos de error de Informes fiscales](/es/errors/index-tax-reports) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # requires_annulment (/es/errors/requires_annulment) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | --------------------------------------- | | `requires_annulment` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El contenido regenerado cambia un campo que entra en la huella —NIF del emisor, serie y número, fecha de expedición, tipo de factura, cuota o importe total— y la cadena no se puede reescribir. ## Qué hacer [#qué-hacer] Anula el registro y da de alta una factura nueva, o una rectificativa, con los datos correctos. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # resource_already_exists (/es/errors/resource_already_exists) | Code | Type | HTTP | Categoría | | ------------------------- | ---------------- | ---- | ----------------------------------- | | `resource_already_exists` | `conflict_error` | 409 | [Request](/es/errors/index-request) | ## Causa [#causa] Crear el objeto duplicaría uno que ya existe bajo una clave única — NIF, SKU, external id. `error.details.existing_resource_id` apunta al objeto que ya ocupa ese valor. ## Qué hacer [#qué-hacer] Actualiza el objeto que devuelve `existing_resource_id`, o envía otro valor en el campo que debe ser único. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # resource_conflict (/es/errors/resource_conflict) | Code | Type | HTTP | Categoría | | ------------------- | ---------------- | ---- | ----------------------------------- | | `resource_conflict` | `conflict_error` | 409 | [Request](/es/errors/index-request) | ## Causa [#causa] La operación chocó con el estado actual del recurso y no aplica ningún código de conflicto más específico. ## Qué hacer [#qué-hacer] Vuelve a leer el recurso, aplica tu cambio sobre el estado que acabas de leer y repite la operación. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # resource_immutable (/es/errors/resource_immutable) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | ----------------------------------- | | `resource_immutable` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] El objeto está cerrado a cambios para esta operación: su estado o su registro contable impiden modificarlo. ## Qué hacer [#qué-hacer] Lee `error.subcode` para saber qué regla lo cerró; la vía habitual es emitir un documento nuevo en vez de editar este. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # resource_locked (/es/errors/resource_locked) | Code | Type | HTTP | Categoría | | ----------------- | ---------------- | ---- | ----------------------------------- | | `resource_locked` | `conflict_error` | 409 | [Request](/es/errors/index-request) | ## Causa [#causa] Otra operación retiene el recurso hasta terminar: las escrituras concurrentes sobre el mismo objeto se serializan en lugar de entrelazarse. ## Qué hacer [#qué-hacer] Reintenta tras una espera breve y reutiliza la misma `Idempotency-Key`, para que el reintento no pueda duplicar la escritura. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # resource_not_deletable (/es/errors/resource_not_deletable) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `resource_not_deletable` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] El objeto existe, pero su estado o sus dependientes bloquean el borrado. En los borrados masivos este es el código por fila de cada entrada que no se pudo eliminar. ## Qué hacer [#qué-hacer] Lee el `reason` de cada fila fallida, elimina o reasigna los dependientes, y repite el borrado solo para esas filas. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # resource_not_found (/es/errors/resource_not_found) | Code | Type | HTTP | Categoría | | -------------------- | ----------------- | ---- | ----------------------------------- | | `resource_not_found` | `not_found_error` | 404 | [Request](/es/errors/index-request) | ## Causa [#causa] El identificador no resuelve a nada visible para la empresa autenticada. Los objetos de otra empresa responden exactamente igual, a propósito. ## Qué hacer [#qué-hacer] Revisa el `id` y el perfil activo (`X-Active-Profile`); lista la colección para confirmar que el objeto existe para esta empresa. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > El recurso solicitado no existe. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # route_not_found (/es/errors/route_not_found) | Code | Type | HTTP | Categoría | | ----------------- | ----------------- | ---- | ----------------------------------- | | `route_not_found` | `not_found_error` | 404 | [Request](/es/errors/index-request) | ## Causa [#causa] La ruta no corresponde a ningún endpoint de v1. Suele ser una errata, un prefijo `/v1` ausente o una ruta de otra área de la API. ## Qué hacer [#qué-hacer] Comprueba la ruta en la referencia de la API, incluida la URL base (`https://api.factuarea.com/v1`). ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Recurso no encontrado. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # scheduled_for_in_past (/es/errors/scheduled_for_in_past) | Code | Type | HTTP | Categoría | | ----------------------- | ----------------------- | ---- | ------------------------------------- | | `scheduled_for_in_past` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] `scheduled_for` no es estrictamente futuro, así que no hay ninguna espera que reservar. ## Qué hacer [#qué-hacer] Envía `scheduled_for` como un instante posterior a ahora, en ISO 8601 con zona horaria; para emitir ya, usa la operación de emisión. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # scope_not_allowed_by_plan (/es/errors/scope_not_allowed_by_plan) | Code | Type | HTTP | Categoría | | --------------------------- | --------------------- | ---- | ---------------------------------------------- | | `scope_not_allowed_by_plan` | `authorization_error` | 422 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] Uno de los scopes pedidos pertenece a un módulo que el plan no incluye, así que la clave nacería con un permiso que nunca podría ejercer. ## Qué hacer [#qué-hacer] Emite la clave sin ese scope, o sube de plan antes de incluirlo. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # scope_not_allowed_in_sandbox (/es/errors/scope_not_allowed_in_sandbox) | Code | Type | HTTP | Categoría | | ------------------------------ | --------------------- | ---- | ---------------------------------------------- | | `scope_not_allowed_in_sandbox` | `authorization_error` | 422 | [Autorización](/es/errors/index-authorization) | ## Causa [#causa] Una clave de prueba no puede nacer con scopes de módulos vetados en sandbox. ## Qué hacer [#qué-hacer] Quita esos scopes de la clave de prueba y resérvalos para la clave de producción que operará sobre la empresa real. ## Relacionado [#relacionado] * [Todos los códigos de error de Autorización](/es/errors/index-authorization) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # seat_charge_failed (/es/errors/seat_charge_failed) | Code | Type | HTTP | Categoría | | -------------------- | ------------------------ | ---- | -------------------------------------- | | `seat_charge_failed` | `payment_required_error` | 402 | [Empresas](/es/errors/index-companies) | ## Causa [#causa] El cobro inmediato del prorrateo del asiento fue rechazado: la tarjeta se denegó, necesita autenticación, o el proveedor de pago estaba inaccesible. La empresa no se crea si el asiento no se cobra. ## Qué hacer [#qué-hacer] Arregla el método de pago en el portal de facturación y repite la operación; consulta con tu banco si la tarjeta se sigue denegando. ## Relacionado [#relacionado] * [Todos los códigos de error de Empresas](/es/errors/index-companies) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # send_failed (/es/errors/send_failed) | Code | Type | HTTP | Categoría | | ------------- | ----------- | ---- | ----------------------------------- | | `send_failed` | `api_error` | 500 | [Servidor](/es/errors/index-server) | ## Causa [#causa] El documento no se entregó por email: el proveedor de correo rechazó el mensaje o estaba inaccesible. ## Qué hacer [#qué-hacer] Revisa la dirección del destinatario y repite el envío; el documento no queda afectado, solo su entrega. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_already_archived (/es/errors/series_already_archived) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | --------------------------------- | | `series_already_archived` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] La serie ya estaba archivada, y el archivado no se repite: una segunda llamada indica que el cliente ha perdido el estado real. ## Qué hacer [#qué-hacer] Lee la marca `is_archived` de la serie antes de actuar; para recuperarla usa la operación de desarchivado. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_code_immutable_with_documents (/es/errors/series_code_immutable_with_documents) | Code | Type | HTTP | Categoría | | -------------------------------------- | ----------------------- | ---- | --------------------------------- | | `series_code_immutable_with_documents` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] Cambiar el prefijo de una serie que ya emitió documentos reescribiría retroactivamente su identificador fiscal, mientras los clientes y la AEAT tienen el número original. ## Qué hacer [#qué-hacer] Crea una serie nueva con el prefijo nuevo y emite desde ella; la antigua conserva los documentos que ya numeró. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_has_documents (/es/errors/series_has_documents) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | --------------------------------- | | `series_has_documents` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] La serie ya numeró documentos, así que no se puede eliminar: la secuencia correlativa tiene que seguir siendo auditable. ## Qué hacer [#qué-hacer] Archiva la serie en lugar de borrarla: deja de ofrecerse en documentos nuevos y conserva su histórico. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_immutable (/es/errors/series_immutable) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | --------------------------------- | | `series_immutable` | `invalid_request_error` | 405 | [Series](/es/errors/index-series) | ## Causa [#causa] Las series no son editables ni eliminables vía API: la continuidad legal de la numeración exige que su prefijo, su año y su contador se queden como están. ## Qué hacer [#qué-hacer] Crea una serie nueva con los valores que necesites, y usa las operaciones de archivado y desarchivado para decidir cuál está en juego. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_initial_number_creates_gap (/es/errors/series_initial_number_creates_gap) | Code | Type | HTTP | Categoría | | ----------------------------------- | ----------------------- | ---- | --------------------------------- | | `series_initial_number_creates_gap` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El número inicial salta más allá del siguiente correlativo natural habiendo documentos del año en curso, y ese hueco en la secuencia no es admisible para la AEAT. ## Qué hacer [#qué-hacer] Fija el número inicial en el siguiente correlativo, o abre una serie nueva si necesitas arrancar desde otro punto. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_locked_by_verifactu (/es/errors/series_locked_by_verifactu) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------------- | ---- | --------------------------------- | | `series_locked_by_verifactu` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] Al menos una factura de la serie tiene un registro de facturación aceptado por la AEAT, lo que congela el prefijo, el año y la base de numeración de la serie. ## Qué hacer [#qué-hacer] Crea una serie nueva para el cambio que necesitas; en esta solo siguen siendo editables el nombre y la política de reinicio del contador. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_not_found (/es/errors/series_not_found) | Code | Type | HTTP | Categoría | | ------------------ | ----------------- | ---- | --------------------------------- | | `series_not_found` | `not_found_error` | 404 | [Series](/es/errors/index-series) | ## Causa [#causa] El identificador no resuelve a ninguna serie de numeración de la empresa autenticada. ## Qué hacer [#qué-hacer] Lista las series, o localiza una por su código indicando el tipo de documento si ese código se repite entre tipos. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_type_invalid (/es/errors/series_type_invalid) | Code | Type | HTTP | Categoría | | --------------------- | ----------------------- | ---- | --------------------------------- | | `series_type_invalid` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] El tipo de documento de la serie queda fuera del catálogo `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. ## Qué hacer [#qué-hacer] Envía uno de los valores del catálogo: una serie numera exactamente un tipo de documento. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # series_year_locked (/es/errors/series_year_locked) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | --------------------------------- | | `series_year_locked` | `invalid_request_error` | 422 | [Series](/es/errors/index-series) | ## Causa [#causa] La serie ya emitió documentos en su año vigente. Mover el año dejaría esos documentos apuntando a un ejercicio vacío mientras su base imponible está en otro. ## Qué hacer [#qué-hacer] Archiva la serie del año en curso y crea una nueva para el ejercicio destino. ## Relacionado [#relacionado] * [Todos los códigos de error de Series](/es/errors/index-series) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # service_unavailable (/es/errors/service_unavailable) | Code | Type | HTTP | Categoría | | --------------------- | --------------------------- | ---- | ----------------------------------- | | `service_unavailable` | `service_unavailable_error` | 503 | [Servidor](/es/errors/index-server) | ## Causa [#causa] El servicio, o una dependencia que necesita, no puede responder temporalmente. ## Qué hacer [#qué-hacer] Reintenta con backoff exponencial; no cambies el payload, porque la petición en sí es correcta. ## Relacionado [#relacionado] * [Todos los códigos de error de Servidor](/es/errors/index-server) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # signature_payload_too_large (/es/errors/signature_payload_too_large) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | -------------------------------------------- | | `signature_payload_too_large` | `invalid_request_error` | 422 | [Albaranes](/es/errors/index-delivery-notes) | ## Causa [#causa] La imagen de la firma supera el tamaño admitido para el campo. ## Qué hacer [#qué-hacer] Envía la firma como PNG del área de dibujo solamente, sin reescalarla hacia arriba; una firma manuscrita cabe holgadamente en el límite. ## Relacionado [#relacionado] * [Todos los códigos de error de Albaranes](/es/errors/index-delivery-notes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # sii_excluded (/es/errors/sii_excluded) | Code | Type | HTTP | Categoría | | -------------- | ----------------------- | ---- | --------------------------------------- | | `sii_excluded` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La empresa está registrada en el SII, y los obligados al SII quedan excluidos del reglamento VeriFactu. ## Qué hacer [#qué-hacer] Sigue declarando por el SII; si el registro en el SII ya no refleja la realidad, corrígelo en la empresa antes de activar VeriFactu. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # simplified_invoice_cannot_be_substituted (/es/errors/simplified_invoice_cannot_be_substituted) | Code | Type | HTTP | Categoría | | ------------------------------------------ | ----------------------- | ---- | ------------------------------------- | | `simplified_invoice_cannot_be_substituted` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Una de las facturas de la lista de sustitución no se puede sustituir: no es simplificada, está cancelada o anulada, pertenece a otra empresa, o ya tiene sustitutiva. ## Qué hacer [#qué-hacer] Quita esa factura de la lista — `error.message` indica el número que bloquea el lote — y vuelve a enviar la sustitución. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # simplified_invoice_not_allowed (/es/errors/simplified_invoice_not_allowed) | Code | Type | HTTP | Categoría | | -------------------------------- | ----------------------- | ---- | ------------------------------------- | | `simplified_invoice_not_allowed` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La operación no es elegible para factura simplificada: supera los 3.000 €, o es una entrega intracomunitaria, una exportación, una operación con inversión del sujeto pasivo, o el cliente necesita factura completa para deducir el IVA. ## Qué hacer [#qué-hacer] Emite una F1 completa identificando al destinatario, o una F3 sustitutiva si la simplificada ya se emitió. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # simplified_limit_exceeded (/es/errors/simplified_limit_exceeded) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `simplified_limit_exceeded` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] Las líneas llevarían la factura simplificada (F2) por encima del tope legal absoluto de 3.000 € IVA incluido. ## Qué hacer [#qué-hacer] Baja el importe, o emite una factura completa (F1) con el destinatario plenamente identificado. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # sku_already_exists (/es/errors/sku_already_exists) | Code | Type | HTTP | Categoría | | -------------------- | ---------------- | ---- | -------------------------------------- | | `sku_already_exists` | `conflict_error` | 409 | [Productos](/es/errors/index-products) | ## Causa [#causa] Otro producto de la empresa ya usa ese SKU, y el SKU identifica al artículo sin ambigüedad dentro del catálogo. ## Qué hacer [#qué-hacer] Actualiza el producto existente —búscalo por SKU— o asigna otro código al nuevo. ## Relacionado [#relacionado] * [Todos los códigos de error de Productos](/es/errors/index-products) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # stripe_payout_already_reconciled (/es/errors/stripe_payout_already_reconciled) | Code | Type | HTTP | Categoría | | ---------------------------------- | ----------------------- | ---- | ---------------------------------- | | `stripe_payout_already_reconciled` | `invalid_request_error` | 422 | [Pagos](/es/errors/index-payments) | ## Causa [#causa] La liquidación ya estaba conciliada, y la conciliación es terminal: repetirla contabilizaría dos veces el apunte bancario. ## Qué hacer [#qué-hacer] Lee la liquidación para ver la conciliación registrada; si es incorrecta, corrige el movimiento bancario con el que se casó. ## Relacionado [#relacionado] * [Todos los códigos de error de Pagos](/es/errors/index-payments) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # stripe_payout_not_found (/es/errors/stripe_payout_not_found) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------- | ---- | ---------------------------------- | | `stripe_payout_not_found` | `not_found_error` | 404 | [Pagos](/es/errors/index-payments) | ## Causa [#causa] El identificador no resuelve a ninguna liquidación de la empresa autenticada. ## Qué hacer [#qué-hacer] Lista las liquidaciones para obtener un `id` vigente; aparecen cuando Stripe las reporta, no en el momento del cobro. ## Relacionado [#relacionado] * [Todos los códigos de error de Pagos](/es/errors/index-payments) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # suplido_line_cannot_carry_taxes (/es/errors/suplido_line_cannot_carry_taxes) | Code | Type | HTTP | Categoría | | --------------------------------- | ----------------------- | ---- | ------------------------------------- | | `suplido_line_cannot_carry_taxes` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La línea de suplido lleva carga propia: tipo de IVA, retención, recargo de equivalencia, descuento, clave de régimen, causa de exención o producto/pack. Un suplido no es una operación del emisor, así que repercutir un impuesto sobre él sería tributar por una entrega que no has hecho, y ligarlo a un producto movería un stock que nunca has vendido. ## Qué hacer [#qué-hacer] Deja la línea a cero en `tax_rate`, `retention_rate`, `surcharge_rate` y `discount_percent` — `tax_rate: 0` explícito, porque omitirlo aplica el 21 % por defecto — y quita `product_id`, `pack_id`, `regime_key` y `exemption_reason`; `error.details.offending_field` nombra el campo que sobra. Si el importe sí lleva IVA tuyo, la línea es `NORMAL`. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # suplido_not_allowed_in_simplified_invoice (/es/errors/suplido_not_allowed_in_simplified_invoice) | Code | Type | HTTP | Categoría | | ------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `suplido_not_allowed_in_simplified_invoice` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La factura es simplificada (F2) y una simplificada no identifica al destinatario. Sin destinatario identificado no hay a quién acreditar el pago por cuenta ajena, así que el importe no admite el tratamiento de suplido en este tipo de factura. ## Qué hacer [#qué-hacer] Emite una factura completa (F1) identificando al cliente para incluir el suplido, o deja el suplido fuera de la simplificada y repercútelo en una factura aparte. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # suplido_requires_source_invoice_reference (/es/errors/suplido_requires_source_invoice_reference) | Code | Type | HTTP | Categoría | | ------------------------------------------- | ----------------------- | ---- | ------------------------------------- | | `suplido_requires_source_invoice_reference` | `invalid_request_error` | 422 | [Facturas](/es/errors/index-invoices) | ## Causa [#causa] La línea de suplido no informa `source_invoice_reference`, el número del justificante que el tercero expidió a nombre del cliente. Sin ese justificante el pago no se acredita como hecho por cuenta ajena y Hacienda lo trataría como base imponible propia del emisor, con su IVA repercutido. ## Qué hacer [#qué-hacer] Añade el número de la factura o de la tasa emitida a nombre del cliente. Si el justificante está a tu nombre, no es un suplido: factúralo como línea `NORMAL` con su tipo de IVA. ## Relacionado [#relacionado] * [Todos los códigos de error de Facturas](/es/errors/index-invoices) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # supplier_has_documents (/es/errors/supplier_has_documents) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------------- | | `supplier_has_documents` | `invalid_request_error` | 422 | [Proveedores](/es/errors/index-suppliers) | ## Causa [#causa] El proveedor está referenciado por facturas de compra registradas, y borrarlo dejaría esos gastos sin la parte que los emitió. ## Qué hacer [#qué-hacer] Desactiva el proveedor en lugar de borrarlo: deja de aparecer en los selectores y sus facturas conservan la referencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Proveedores](/es/errors/index-suppliers) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # supplier_not_found (/es/errors/supplier_not_found) | Code | Type | HTTP | Categoría | | -------------------- | ----------------- | ---- | ----------------------------------------- | | `supplier_not_found` | `not_found_error` | 404 | [Proveedores](/es/errors/index-suppliers) | ## Causa [#causa] El identificador no resuelve a ningún proveedor de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id`, o busca el proveedor por `tax_id` o por `external_id` antes de crear un duplicado. ## Relacionado [#relacionado] * [Todos los códigos de error de Proveedores](/es/errors/index-suppliers) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # system_tax_default_modification_forbidden (/es/errors/system_tax_default_modification_forbidden) | Code | Type | HTTP | Categoría | | ------------------------------------------- | --------------------- | ---- | ----------------------------------- | | `system_tax_default_modification_forbidden` | `authorization_error` | 403 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] Los defaults de los impuestos del catálogo compartido no se fijan sobre el impuesto: el catálogo es global y la preferencia es de tu empresa. ## Qué hacer [#qué-hacer] Fija el default por el endpoint de defaults fiscales de la empresa (`POST /v1/companies/me/tax-defaults`), no por el del impuesto. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # system_tax_immutable (/es/errors/system_tax_immutable) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------------- | ---- | ----------------------------------- | | `system_tax_immutable` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El impuesto pertenece al catálogo canónico AEAT que trae el producto. Su tipo, su código y su nombre son fijos para que todas las empresas compartan la misma referencia fiscal. ## Qué hacer [#qué-hacer] Crea un impuesto propio con los valores que necesites, o usa las operaciones que sí admiten los impuestos del sistema: activar, desactivar y marcarlos como default. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # system_tax_immutable_field (/es/errors/system_tax_immutable_field) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------------- | ---- | ----------------------------------- | | `system_tax_immutable_field` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La actualización toca un campo congelado en un impuesto del sistema; `error.param` dice cuál. ## Qué hacer [#qué-hacer] Quita ese campo del payload: en los impuestos del sistema solo cambian la marca de activo y las de default. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # system_tax_undeletable (/es/errors/system_tax_undeletable) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `system_tax_undeletable` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] Los impuestos del sistema forman parte del catálogo fiscal compartido y no se eliminan: borrarlos rompería los documentos que los referencian. ## Qué hacer [#qué-hacer] Desactiva el impuesto si no quieres que se siga ofreciendo; al desactivarlo se limpian además sus marcas de default. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_applies_to_invalid (/es/errors/tax_applies_to_invalid) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `tax_applies_to_invalid` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El ámbito del impuesto queda fuera del catálogo `sale`, `purchase`, `both`. ## Qué hacer [#qué-hacer] Envía `sale` para impuestos repercutidos en ventas, `purchase` para los soportados en compras, o `both` cuando aplique en ambos lados. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_code_already_exists (/es/errors/tax_code_already_exists) | Code | Type | HTTP | Categoría | | ------------------------- | ---------------- | ---- | ----------------------------------- | | `tax_code_already_exists` | `conflict_error` | 409 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] Otro impuesto del catálogo ya usa ese código, y el código identifica al impuesto sin ambigüedad. ## Qué hacer [#qué-hacer] Reutiliza el impuesto existente, o elige otro código para el nuevo. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_id_already_exists (/es/errors/tax_id_already_exists) | Code | Type | HTTP | Categoría | | ----------------------- | ---------------- | ---- | ------------------------------------ | | `tax_id_already_exists` | `conflict_error` | 409 | [Clientes](/es/errors/index-clients) | ## Causa [#causa] Otro cliente de la empresa ya tiene ese NIF, y el NIF identifica a la parte sin ambigüedad dentro de una empresa. ## Qué hacer [#qué-hacer] Reutiliza el cliente existente —búscalo por NIF— o corrige el valor si se tecleó mal. ## Relacionado [#relacionado] * [Todos los códigos de error de Clientes](/es/errors/index-clients) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_id_required (/es/errors/tax_id_required) | Code | Type | HTTP | Categoría | | ----------------- | ----------------------- | ---- | ----------------------------------- | | `tax_id_required` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] La operación necesita el número de identificación fiscal (NIF, CIF o NIE) de la parte implicada y el registro no lo tiene. ## Qué hacer [#qué-hacer] Rellena `tax_id` en el cliente, el proveedor o la empresa antes de repetir la operación. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_in_use (/es/errors/tax_in_use) | Code | Type | HTTP | Categoría | | ------------ | ----------------------- | ---- | ----------------------------------- | | `tax_in_use` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El impuesto está referenciado por documentos, productos o proveedores. Eliminarlo dejaría documentos históricos sin su referencia fiscal. ## Qué hacer [#qué-hacer] Desactívalo en lugar de borrarlo: deja de ofrecerse en documentos nuevos y los existentes conservan su referencia. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_inactive_cannot_be_default (/es/errors/tax_inactive_cannot_be_default) | Code | Type | HTTP | Categoría | | -------------------------------- | ----------------------- | ---- | ----------------------------------- | | `tax_inactive_cannot_be_default` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] Un impuesto desactivado no puede quedar como default, ni global ni por tipo de documento: sería un default oculto que ningún formulario puede elegir. ## Qué hacer [#qué-hacer] Activa antes el impuesto y márcalo después como default. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_not_found (/es/errors/tax_not_found) | Code | Type | HTTP | Categoría | | --------------- | ----------------- | ---- | ----------------------------------- | | `tax_not_found` | `not_found_error` | 404 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El identificador no corresponde a ningún impuesto del catálogo accesible para esta empresa. ## Qué hacer [#qué-hacer] Lista el catálogo y usa el `id` que devuelve; el impuesto también puede quedar fuera por la zona AEAT de tu empresa. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_report_not_found (/es/errors/tax_report_not_found) | Code | Type | HTTP | Categoría | | ---------------------- | ----------------- | ---- | ------------------------------------------------- | | `tax_report_not_found` | `not_found_error` | 404 | [Informes fiscales](/es/errors/index-tax-reports) | ## Causa [#causa] El identificador no resuelve a ninguna declaración de la empresa autenticada. ## Qué hacer [#qué-hacer] Lista las declaraciones para obtener un `id` vigente, o genera la del período antes de leerla. ## Relacionado [#relacionado] * [Todos los códigos de error de Informes fiscales](/es/errors/index-tax-reports) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_report_type_invalid (/es/errors/tax_report_type_invalid) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ------------------------------------------------- | | `tax_report_type_invalid` | `invalid_request_error` | 422 | [Informes fiscales](/es/errors/index-tax-reports) | ## Causa [#causa] El tipo de declaración queda fuera del catálogo `modelo_303`, `modelo_347`, `modelo_130`. ## Qué hacer [#qué-hacer] Envía el modelo que necesitas: 303 IVA trimestral, 130 pago fraccionado del IRPF, 347 operaciones anuales con terceros. ## Relacionado [#relacionado] * [Todos los códigos de error de Informes fiscales](/es/errors/index-tax-reports) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # tax_type_invalid (/es/errors/tax_type_invalid) | Code | Type | HTTP | Categoría | | ------------------ | ----------------------- | ---- | ----------------------------------- | | `tax_type_invalid` | `invalid_request_error` | 422 | [Impuestos](/es/errors/index-taxes) | ## Causa [#causa] El tipo de impuesto queda fuera del catálogo `vat`, `retention`, `surcharge`, `other`. ## Qué hacer [#qué-hacer] Envía uno de los cuatro tipos: decide el rango de tipo impositivo admitido y cómo participa el impuesto en los totales. ## Relacionado [#relacionado] * [Todos los códigos de error de Impuestos](/es/errors/index-taxes) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # timeout_seconds_out_of_range (/es/errors/timeout_seconds_out_of_range) | Code | Type | HTTP | Categoría | | ------------------------------ | ----------------------- | ---- | ------------------------------------- | | `timeout_seconds_out_of_range` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] `timeout_seconds` queda fuera del rango de 1 a 30 segundos. ## Qué hacer [#qué-hacer] Envía un valor dentro del rango; si tu receptor necesita más, confirma el evento de inmediato y procésalo de forma asíncrona en tu lado. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # too_many_auth_failures (/es/errors/too_many_auth_failures) | Code | Type | HTTP | Categoría | | ------------------------ | ---------------------- | ---- | ------------------------------------------------ | | `too_many_auth_failures` | `authentication_error` | 429 | [Autenticación](/es/errors/index-authentication) | ## Causa [#causa] Llegaron demasiados intentos fallidos de autenticación desde la misma dirección, así que queda bloqueada temporalmente para frenar los intentos de adivinar credenciales. ## Qué hacer [#qué-hacer] Detén los reintentos, corrige la clave y espera cinco minutos antes de volver a probar. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Demasiados intentos fallidos de autenticación desde esta IP. Espera 5 minutos antes de reintentar. ## Relacionado [#relacionado] * [Todos los códigos de error de Autenticación](/es/errors/index-authentication) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # too_many_custom_headers (/es/errors/too_many_custom_headers) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ------------------------------------- | | `too_many_custom_headers` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El endpoint declara más de 20 cabeceras personalizadas. ## Qué hacer [#qué-hacer] Deja solo las cabeceras que tu receptor necesita de verdad; la autenticación suele caber en una. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # unknown_filter (/es/errors/unknown_filter) | Code | Type | HTTP | Categoría | | ---------------- | ----------------------- | ---- | ----------------------------------- | | `unknown_filter` | `invalid_request_error` | 422 | [Request](/es/errors/index-request) | ## Causa [#causa] Un listado recibió un filtro que no conoce. Los parsers canónicos de v1 reportan esto como `parameter_unknown`; este código sobrevive para los endpoints aún sin migrar. ## Qué hacer [#qué-hacer] Quita el filtro, o sustitúyelo por uno de los campos que el endpoint documenta como filtrables. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # unsupported_api_version (/es/errors/unsupported_api_version) | Code | Type | HTTP | Categoría | | ------------------------- | ----------------------- | ---- | ----------------------------------- | | `unsupported_api_version` | `invalid_request_error` | 400 | [Request](/es/errors/index-request) | ## Causa [#causa] La cabecera `Factuarea-Version` está bien formada pero nombra una versión fuera del conjunto soportado. ## Qué hacer [#qué-hacer] Envía una de las fechas de versión soportadas, u omite la cabecera para usar la versión fijada en tu API key. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # unsupported_format (/es/errors/unsupported_format) | Code | Type | HTTP | Categoría | | -------------------- | ----------------------- | ---- | ------------------------------------------------- | | `unsupported_format` | `invalid_request_error` | 422 | [Informes fiscales](/es/errors/index-tax-reports) | ## Causa [#causa] El formato pedido no está disponible para este modelo: no toda declaración produce todas las salidas. ## Qué hacer [#qué-hacer] Pide uno de los formatos que sí ofrece el modelo: el fichero de texto para la AEAT, el PDF o la hoja de cálculo. ## Relacionado [#relacionado] * [Todos los códigos de error de Informes fiscales](/es/errors/index-tax-reports) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # unsupported_media_type (/es/errors/unsupported_media_type) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | ----------------------------------- | | `unsupported_media_type` | `invalid_request_error` | 415 | [Request](/es/errors/index-request) | ## Causa [#causa] Una petición con body declaró un `Content-Type` distinto de `application/json`. ## Qué hacer [#qué-hacer] Fija `Content-Type: application/json` y serializa el body como JSON. ## Mensaje que devuelve la API [#mensaje-que-devuelve-la-api] > Solo se acepta Content-Type application/json. ## Relacionado [#relacionado] * [Todos los códigos de error de Request](/es/errors/index-request) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # verifactu_already_submitted (/es/errors/verifactu_already_submitted) | Code | Type | HTTP | Categoría | | ----------------------------- | ----------------------- | ---- | --------------------------------------- | | `verifactu_already_submitted` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La factura ya tiene su registro de alta. Existe exactamente un alta por factura, así que una segunda rompería la idempotencia de la cadena. ## Qué hacer [#qué-hacer] Lee el registro existente en lugar de crear otro; para cambiar lo declarado, emite una rectificativa. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # verifactu_mode_invalid (/es/errors/verifactu_mode_invalid) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `verifactu_mode_invalid` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El modo queda fuera del catálogo `verifactu` / `no_verifactu`. ## Qué hacer [#qué-hacer] Envía `verifactu` para declarar a la AEAT en tiempo real, o `no_verifactu` para el modo de registro local. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # verifactu_not_eligible (/es/errors/verifactu_not_eligible) | Code | Type | HTTP | Categoría | | ------------------------ | ----------------------- | ---- | --------------------------------------- | | `verifactu_not_eligible` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] La factura no se puede registrar ahora mismo en la AEAT: la empresa no está en modo VeriFactu, no tiene certificado activo, o el certificado está revocado o emitido para otro NIF. ## Qué hacer [#qué-hacer] Activa el modo VeriFactu y sube un certificado FNMT válido cuyo NIF coincida con el de la empresa; los registros diferidos por este motivo se reencolan en cuanto hay certificado válido. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # verifactu_record_not_found (/es/errors/verifactu_record_not_found) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------- | ---- | --------------------------------------- | | `verifactu_record_not_found` | `not_found_error` | 404 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El identificador no corresponde a ningún registro de facturación de la empresa autenticada. ## Qué hacer [#qué-hacer] Revisa el `id`, o localiza el registro por su CSV, por su huella o por el número de la factura que lo generó. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # verifactu_transmission_failed (/es/errors/verifactu_transmission_failed) | Code | Type | HTTP | Categoría | | ------------------------------- | ----------------------- | ---- | --------------------------------------- | | `verifactu_transmission_failed` | `invalid_request_error` | 422 | [VeriFactu](/es/errors/index-verifactu) | ## Causa [#causa] El envío del registro a la AEAT no llegó a completarse: el endpoint estaba inaccesible o respondió con una incidencia. ## Qué hacer [#qué-hacer] Consulta el estado del registro — la transmisión se reintenta sola con backoff exponencial — y fuerza un reintento cuando haya pasado la ventana de espera. ## Relacionado [#relacionado] * [Todos los códigos de error de VeriFactu](/es/errors/index-verifactu) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # webhook_delivery_not_found (/es/errors/webhook_delivery_not_found) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------- | ---- | ------------------------------------- | | `webhook_delivery_not_found` | `not_found_error` | 404 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El identificador no corresponde a ningún intento de entrega, o la entrega queda fuera de la ventana de retención del histórico. ## Qué hacer [#qué-hacer] Lista las entregas del endpoint para obtener un `id` vigente; las entregas anteriores a la ventana de retención ya no están disponibles. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # webhook_endpoint_degraded (/es/errors/webhook_endpoint_degraded) | Code | Type | HTTP | Categoría | | --------------------------- | ----------------------- | ---- | ------------------------------------- | | `webhook_endpoint_degraded` | `invalid_request_error` | 422 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El endpoint está degradado tras fallos repetidos de entrega, así que los pings de prueba se rechazan mientras siga en ese estado. ## Qué hacer [#qué-hacer] Arregla el receptor, reactiva el endpoint y envía después el ping de prueba. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # webhook_endpoint_not_found (/es/errors/webhook_endpoint_not_found) | Code | Type | HTTP | Categoría | | ---------------------------- | ----------------- | ---- | ------------------------------------- | | `webhook_endpoint_not_found` | `not_found_error` | 404 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El identificador no resuelve a ningún endpoint de webhook de la empresa autenticada. ## Qué hacer [#qué-hacer] Lista tus endpoints y usa el `id` que devuelven. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # webhook_secret_recently_rotated (/es/errors/webhook_secret_recently_rotated) | Code | Type | HTTP | Categoría | | --------------------------------- | ------------------ | ---- | ------------------------------------- | | `webhook_secret_recently_rotated` | `rate_limit_error` | 429 | [Webhooks](/es/errors/index-webhooks) | ## Causa [#causa] El secreto de firma se rotó hace menos de cinco minutos. La ventana de gracia permite que tu receptor acepte ambos secretos durante el cambio; rotar otra vez dentro de ella invalidaría firmas todavía en vuelo. ## Qué hacer [#qué-hacer] Espera cinco minutos desde la última rotación, y despliega el secreto nuevo en tu receptor antes de volver a rotar. ## Relacionado [#relacionado] * [Todos los códigos de error de Webhooks](/es/errors/index-webhooks) * [Códigos de error por categoría](/es/errors) * [Tabla de referencia completa](/es/guides/errors/all) * [Modelo de errores](/es/guides/errors) --- # FAQ (/es/faq) Respuestas breves a las preguntas que más surgen al desarrollar contra la API pública de Factuarea. Cada una enlaza con la guía que la cubre por completo. ## Acceso y claves [#acceso-y-claves] ### ¿Cómo consigo acceso a la API? [#cómo-consigo-acceso-a-la-api] La API está **incluida en todos los planes de Factuarea** — sin add-on aparte ni solicitud de acceso. Crea tu API key desde [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) y empieza a llamar a `/v1`. Durante el trial de 10 días ya tienes acceso con el tier `free`; los planes de pago suben el tier de rate limit. Consulta [Límites de peticiones](/guides/rate-limits). ### ¿Esta clave es live o test? [#esta-clave-es-live-o-test] Lee el **prefijo**: `fact_live_` opera sobre tu empresa real (producción), `fact_test_` sobre un sandbox aislado. El prefijo es la única fuente de verdad — ningún parámetro de la petición cambia el entorno. Consulta [Modo de prueba y sandbox](/guides/test-mode). ### He perdido el secreto de mi API key. ¿Puedo recuperarlo? [#he-perdido-el-secreto-de-mi-api-key-puedo-recuperarlo] No. El backend solo almacena un hash bcrypt del secreto, que se muestra **una sola vez** al crearlo. Rota la clave desde el dashboard para emitir un nuevo secreto y vuelve a desplegarlo. Consulta [Autenticación › Rotación](/guides/authentication). ### ¿Cómo roto una clave sin downtime? [#cómo-roto-una-clave-sin-downtime] Rota desde el dashboard: el secreto antiguo y el nuevo permanecen válidos durante un **periodo de gracia**, así que puedes desplegar el nuevo sin perder peticiones. Revoca una clave solo cuando sospeches una fuga — eso la invalida al instante (`401 api_key_revoked`). Consulta [Autenticación › Rotación y revocación](/guides/authentication). ## Modo de prueba [#modo-de-prueba] ### ¿Los datos de test están aislados de producción? [#los-datos-de-test-están-aislados-de-producción] Sí — de forma estructural, no por un filtro. Una clave `fact_test_` opera sobre una **empresa sandbox** dedicada, así que los recursos creados en test nunca son visibles para una clave `fact_live_` (y viceversa), y la numeración fiscal de test nunca toca tus series de producción. Consulta [Modo de prueba › Aislamiento de datos](/guides/test-mode). ### ¿Por qué no se disparan mis webhooks en modo de prueba? [#por-qué-no-se-disparan-mis-webhooks-en-modo-de-prueba] En test, los efectos externos están desactivados: VeriFactu → AEAT, FACe, los emails y la **entrega de webhooks** están todos neutralizados. Los eventos se siguen registrando con `livemode: false` y se pueden consultar vía `GET /v1/events`, pero no se entregan a tus endpoints. Usa `POST /v1/webhook_endpoints/{id}/ping` para probar tu receptor. Consulta [Modo de prueba › Qué está desactivado](/guides/test-mode). ## Documentos [#documentos] ### ¿Qué diferencia hay entre delete, annul y void de una factura? [#qué-diferencia-hay-entre-delete-annul-y-void-de-una-factura] `DELETE /v1/invoices/{id}` solo funciona con **borradores**. Una vez emitida una factura no se puede eliminar: usa `POST /v1/invoices/{id}/annul` (registra una razón documentada y crea el registro de *anulación* de AEAT cuando VeriFactu está activado) o `POST /v1/invoices/{id}/void` (irreversible, registra un `void_reason`, rechazado si la factura ya ha sido rectificada). Consulta [Migración desde Holded › Diferencias intencionadas](/guides/migration-from-holded). ## Dinero y fechas [#dinero-y-fechas] ### ¿Cómo se representan los importes monetarios? [#cómo-se-representan-los-importes-monetarios] En **euros**, con dos decimales, al estilo Stripe — la forma canónica es una cadena decimal como `"1234.56"`. Parsea el dinero como decimal fijo, nunca como float binario, y deja que la API calcule los totales a partir de las líneas en bruto. Consulta [Importes y fechas › Dinero](/guides/amounts-and-dates). ### ¿Qué formato usan las fechas? [#qué-formato-usan-las-fechas] Las fechas de calendario como `issued_on` y `due_on` usan `YYYY-MM-DD` (por ejemplo `2026-05-15`). Las marcas de tiempo como `created` y `expires_at` son **ISO 8601 en UTC** con sufijo `Z` — p. ej. `2026-05-15T10:23:18Z`. Consulta [Importes y fechas](/guides/amounts-and-dates). ### ¿Qué zona horaria usan las cuotas? [#qué-zona-horaria-usan-las-cuotas] La **cuota mensual del límite de peticiones** se reinicia el día 1 a las 00:00 **Europe/Madrid**, mientras que las marcas de tiempo se devuelven en UTC. Consulta [Importes y fechas › Zona horaria de las cuotas](/guides/amounts-and-dates) y [Límites de peticiones](/guides/rate-limits). ## Idempotencia y reintentos [#idempotencia-y-reintentos] ### ¿Qué pasa si reenvío una Idempotency-Key? [#qué-pasa-si-reenvío-una-idempotency-key] Dentro del TTL de 24h, la API devuelve la respuesta **cacheada** (estado, headers y body) sin volver a ejecutar el handler, añadiendo el header `Idempotent-Replayed: true`. Un `4xx` cacheado también se reenvía. Reutilizar la clave con un body **distinto** responde `409 idempotency_key_reused`. Consulta [Idempotencia](/guides/idempotency). ### ¿Un replay idempotente cuenta contra mi rate limit? [#un-replay-idempotente-cuenta-contra-mi-rate-limit] No. Una clave reenviada dentro de su TTL **no cuenta** contra tu cuota. Claves distintas con el mismo payload cuentan cada una, una a una. Consulta [Idempotencia › Qué NO es la idempotencia](/guides/idempotency). ### ¿Cuál es la longitud máxima de la Idempotency-Key? [#cuál-es-la-longitud-máxima-de-la-idempotency-key] Entre **1 y 64 caracteres**. Cualquier valor único opaco sirve (se recomienda UUID v7, pero UUID v4, ULID o nanoid también valen). Consulta [Idempotencia › Formato de la clave](/guides/idempotency). ## Límites de peticiones y errores [#límites-de-peticiones-y-errores] ### ¿Cómo conozco mi cuota restante? [#cómo-conozco-mi-cuota-restante] Cada respuesta (incluida `429`) lleva `X-RateLimit-Limit`, `X-RateLimit-Remaining` y `X-RateLimit-Reset`; un `429` añade `Retry-After` con los segundos que hay que esperar. Los límites dependen del tier de tu clave. Consulta [Límites de peticiones](/guides/rate-limits). ### ¿Cómo debo reintentar una petición fallida? [#cómo-debo-reintentar-una-petición-fallida] `4xx` (excepto `429`) → no reintentes, corrige la petición. `429` → respeta `Retry-After`. `5xx` → back-off exponencial con jitter, hasta 5 intentos. Consulta [Errores › Estrategia de reintentos](/guides/errors). ### ¿Dónde reporto un problema con una petición concreta? [#dónde-reporto-un-problema-con-una-petición-concreta] Coge el `request_id` del envoltorio de error (también está en el header `X-Request-Id`) y envíalo a soporte — nos permite correlacionar logs, métricas y trazas. Consulta [Soporte](/support). --- # Ausencias (/es/guides/absences) El dominio de **ausencias** tiene dos capas: una capa de **configuración** (qué se puede solicitar y cuánto) y una capa de **flujo** (solicitudes, saldos y calendario). Todo está acotado por `absences:read` / `absences:write` y gateado por el módulo `control_horario`, bajo `https://api.factuarea.com/v1`. ## Tipos de ausencia [#types] Un **tipo de ausencia** es lo que un empleado puede solicitar — vacaciones, baja por enfermedad, un día de asuntos propios. Cada tipo lleva: si es **retribuido** (`is_paid`), si **requiere aprobación** (`requires_approval`), una **unidad de medida** (`days` u `hours`), un **color** hex, una **visibilidad** (`everyone` o `managers_only`) y un estado (`active` / `archived`). El nombre es único por empresa. En cada empresa nueva se **siembra** un set de tipos españoles por defecto, así que sueles arrancar con un catálogo funcional. | Operación | Endpoint | | ---------------------- | ---------------------------------------------------------- | | Listar / detalle | `GET /v1/absence-types`, `GET /v1/absence-types/{type}` | | Crear / actualizar | `POST /v1/absence-types`, `PATCH /v1/absence-types/{type}` | | Archivar / desarchivar | `POST /v1/absence-types/{type}/archive`, `.../unarchive` | ```bash curl -X POST https://api.factuarea.com/v1/absence-types \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Asuntos propios", "is_paid": true, "requires_approval": true, "measurement_unit": "days", "color": "#4F46E5", "visibility": "everyone" }' ``` ## Políticas de ausencia [#policies] Una **política de ausencia** decide **cuánto** y **para quién**. Fija una **asignación de días** — `limited` (un número positivo de días) o `unlimited` —, un **método de devengo** (`annual` o `monthly`), el conjunto de **tipos** que cubre y los **empleados** a los que se asigna. Asociar tipos es un reemplazo total; una política se asigna y desasigna de empleados en lote. | Operación | Endpoint | | ------------------------------ | ------------------------------------------------------------------ | | Listar / detalle | `GET /v1/absence-policies`, `GET /v1/absence-policies/{policy}` | | Crear / actualizar | `POST /v1/absence-policies`, `PATCH /v1/absence-policies/{policy}` | | Asignar / desasignar empleados | `POST /v1/absence-policies/{policy}/assign`, `.../unassign` | | Listar asignaciones | `GET /v1/absence-policies/{policy}/assignments` | | Arrastre | `GET /v1/absence-policies/{policy}/carryover` | | Archivar / desarchivar | `POST /v1/absence-policies/{policy}/archive`, `.../unarchive` | El **arrastre** expone cuánta asignación no consumida pasa al siguiente periodo de devengo por empleado. La asignación siempre resuelve al empleado dentro de la empresa autenticada, así que una política de la empresa A jamás se asigna a un empleado de la empresa B. ```bash curl -X POST https://api.factuarea.com/v1/absence-policies \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Estándar 22 días", "allowance": { "type": "limited", "days": 22 }, "accrual_method": "annual", "absence_type_ids": ["01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b"] }' ``` ## Solicitudes, saldos y calendario [#requests] Cuando existen tipos y políticas, los empleados **solicitan** ausencias y los managers las resuelven. | Operación | Endpoint | Scope | | -------------------- | ----------------------------------------------------------------- | ---------------- | | Crear una solicitud | `POST /v1/absence-requests` | `absences:write` | | Aprobar / rechazar | `POST /v1/absence-requests/{request}/approve`, `.../reject` | `absences:write` | | Cancelar | `POST /v1/absence-requests/{request}/cancel` | `absences:write` | | Listar / detalle | `GET /v1/absence-requests`, `GET /v1/absence-requests/{request}` | `absences:read` | | Saldos | `GET /v1/absence-balances`, `GET /v1/absence-balances/{employee}` | `absences:read` | | Calendario de equipo | `GET /v1/absence-calendar` | `absences:read` | Un **saldo** es la asignación restante por empleado y tipo, derivada del devengo de la política menos las solicitudes aprobadas. El **calendario** devuelve las ausencias del equipo en un rango de fechas — la vista del manager de quién está fuera y cuándo. ```bash curl -X POST https://api.factuarea.com/v1/absence-requests \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "absence_type_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "start_date": "2026-08-01", "end_date": "2026-08-15" }' ``` <Callout type="info"> Un tipo con `requires_approval: false` se concede al solicitarlo; uno con `requires_approval: true` espera a que un manager lo apruebe o lo rechace antes de descontar del saldo. </Callout> ## Flujo típico [#flow] 1. Revisa los **tipos** sembrados, o crea los tuyos. 2. Crea **políticas** con una asignación de días y un devengo, y cubre los tipos pertinentes. 3. **Asigna** cada política a sus empleados. 4. Los empleados **solicitan**; los managers **aprueban** o **rechazan**. 5. Lee **saldos** y el **calendario**, y consulta el **arrastre** al cierre del año. Los festivos que afectan a las ausencias viven en su propio dominio de solo lectura — consulta la [visión general](/guides/workforce-overview) y la [referencia de festivos](/api-reference/holidays/public-api.v1.holidays.list). ## Próximos pasos [#next] * [Cierre mensual](/guides/monthly-time-close) — las ausencias aprobadas alimentan el informe mensual. * Navega la referencia de [tipos](/api-reference/absence-types/public-api.v1.absence-types.list), [políticas](/api-reference/absence-policies/public-api.v1.absence-policies.list) y [solicitudes](/api-reference/absence-requests/public-api.v1.absence-requests.create). --- # Personalización de la cuenta (/es/guides/account-personalization) La personalización controla **el aspecto y la lectura de tus facturas**: el idioma en que se genera el PDF, la plantilla PDF que lo enmarca y el color de acento que lo identifica. Los tres viven en la empresa autenticada y se aplican a todos los documentos que la API genera para ti. Lees los valores actuales desde el bloque `personalization` de `GET /v1/account`, y los cambias con una única actualización parcial en `PATCH /v1/account/personalization`. Ambos endpoints funcionan igual en modo de prueba (claves `fact_test_`) y en producción (claves `fact_live_`). ## Los tres ajustes [#los-tres-ajustes] | Ajuste | Campo | Valores aceptados | | ----------------------------- | -------------- | -------------------------------------------------------- | | Idioma de emisión de facturas | `language` | `es`, `en`, `ca` | | Plantilla PDF | `pdf_template` | `classic`, `modern`, `minimal`, `corporative`, `premium` | | Color de acento | `accent_color` | hexadecimal `#RRGGBB`, o `null` para limpiarlo | ### Idioma de emisión de facturas [#idioma-de-emisión-de-facturas] `language` es el locale en que se genera el **PDF**. Ponlo a `en` y los títulos, etiquetas y fechas de cada PDF que generes pasan a inglés; `ca` los muestra en catalán; `es` (por defecto) en castellano. No cambia el texto `message` de los errores de la API — esos siguen en castellano, como documenta el [modelo de errores](/guides/errors). ### Plantilla PDF [#plantilla-pdf] `pdf_template` es un slug del catálogo cerrado `PdfTemplate`. Las cinco plantillas de sistema son `classic`, `modern` (por defecto), `minimal`, `corporative` y `premium`. Cuáles puede seleccionar tu cuenta depende de tu plan — descubre el conjunto permitido con [el endpoint de plantillas](#descubrir-las-plantillas-disponibles) en lugar de fijarlo a mano. ### Color de acento [#color-de-acento] `accent_color` es el color hexadecimal `#RRGGBB` con que se identifica el PDF (cabeceras, totales, acentos). Envía `null` para limpiarlo y volver al valor por defecto de la plantilla. ## Leer la personalización actual [#leer-la-personalización-actual] El bloque `personalization` forma parte del recurso `Account`: ```bash curl -s https://api.factuarea.com/v1/account \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ | jq '.data.personalization' ``` ```json { "language": "es", "pdf_template": "modern", "accent_color": "#1a73e8" } ``` `language` y `pdf_template` siempre están presentes. `accent_color` es `null` cuando no hay ningún color configurado. ## Actualizar la personalización [#actualizar-la-personalización] `PATCH /v1/account/personalization` es una **actualización parcial**: solo se aplican los campos que envías, y cualquier campo que omitas mantiene su valor actual. La respuesta es el **recurso `Account` actualizado** — con la misma forma que `GET /v1/account`, incluido el bloque `personalization` recién refrescado. Requiere el scope `account:write`. ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "en", "pdf_template": "premium", "accent_color": "#0F766E" }' \ | jq '.data.personalization' ``` Cambia un único ajuste enviando solo ese campo: ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "ca" }' ``` Limpia el color de acento enviando `null`: ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "accent_color": null }' ``` <Callout type="info"> Cada ajuste es independiente: `language`, `pdf_template` y `accent_color` no se pisan entre sí. Enviar uno nunca reinicia los otros dos. </Callout> ### Errores de validación [#errores-de-validación] Cada ajuste se valida contra su catálogo cerrado. Un valor fuera del catálogo devuelve `422` con los `allowed_values` del campo erróneo — `language` y `pdf_template` contra su enum, `accent_color` contra el patrón `#RRGGBB`: ```json { "error": { "type": "validation_error", "code": "validation_failed", "message": "El idioma indicado no es válido.", "param": "language", "allowed_values": ["es", "en", "ca"] } } ``` ## Descubrir las plantillas disponibles [#descubrir-las-plantillas-disponibles] `GET /v1/account/personalization/templates` lista las plantillas PDF disponibles para el plan de tu cuenta (según el plan) junto con el formato aceptado para `accent_color`. Úsalo para poblar un selector en vez de fijar el catálogo a mano. Requiere el scope `account:read`. ```bash curl -s https://api.factuarea.com/v1/account/personalization/templates \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ | jq '.data' ``` ```json { "object": "personalization_templates", "templates": [ { "slug": "classic", "label": "Clásica", "available": true }, { "slug": "modern", "label": "Moderna", "available": true }, { "slug": "minimal", "label": "Minimalista", "available": true }, { "slug": "corporative", "label": "Corporativa", "available": false }, { "slug": "premium", "label": "Premium", "available": false } ], "accent_color": { "format": "#RRGGBB", "example": "#1a73e8" } } ``` El indicador `available` refleja tu **plan actual**: un slug en `false` existe en el catálogo pero no se puede fijar hasta que mejores de plan. Ofrece solo las plantillas disponibles, y lee `accent_color.format` para validar el color en cliente antes del `PATCH`. ## Scopes [#scopes] | Operación | Endpoint | Scope | | ----------------------------- | ------------------------------------------- | --------------- | | Leer la personalización | `GET /v1/account` | `account:read` | | Listar plantillas | `GET /v1/account/personalization/templates` | `account:read` | | Actualizar la personalización | `PATCH /v1/account/personalization` | `account:write` | --- # Actuar en nombre de una hija (/es/guides/acting-on-behalf) Una vez que tienes [empresas gestionadas](/guides/companies) bajo tu tenant maestro, hay dos formas de actuar sobre una de ellas. Puedes emitir una [API key hija](/guides/child-api-keys) ligada a ella — útil cuando quieres una credencial acotada a una sola empresa. O puedes seguir usando tu **master key** y elegir la empresa objetivo por petición con el header `X-Active-Profile`. Así, una sola master key opera sobre cualquier empresa de tu árbol, sin re-autenticarte ni gestionar una key por NIF. Esta página cubre el header. Pon en el header el `id` público (UUID v7) de la empresa hija sobre la que quieres actuar. Funciona sobre **cualquier** endpoint — facturas, clientes, series y el resto: ```bash curl https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "X-Active-Profile: 01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c" ``` Cuando el header está presente y la empresa es tuya, **toda la petición** se ejecuta sobre los datos de esa empresa hija: cada lectura se filtra a ella y cada escritura cae sobre ella. La petición se resuelve al `company_id` de la empresa hija antes del rate limit y la idempotencia, así que cada empresa tiene sus propios buckets. El header es **opcional y aditivo**. Omítelo y la petición opera sobre la empresa a la que pertenece tu key — exactamente como antes. Las integraciones existentes siguen funcionando sin cambios. <Callout type="info"> El header **nunca** amplía tu key. Sus `scopes`, `tier` y `environment` se conservan intactos: una master key con solo `invoices:read` que apunta a una empresa hija sigue sin poder hacer `POST /v1/invoices` ahí (`403 insufficient_scope`), y una key `fact_test_` se mantiene en el sandbox sea cual sea el perfil activo. Cambiar de perfil cambia **sobre qué** empresa actúas, nunca **qué** tienes permitido hacer. La empresa hija hereda el plan y los add-ons del maestro. </Callout> ## Resolución y errores [#resolution] `X-Active-Profile` resuelve la empresa activa antes de que corra ningún handler: | Header | Resultado | | --------------------------------------------------------- | --------------------------------------------------------------------- | | Ausente o vacío | La petición opera sobre la empresa a la que pertenece la key (no-op). | | El `id` de tu propia empresa maestra | Permitido — equivale a omitir el header. | | Una empresa hija que posees, `active` | La petición opera sobre esa empresa hija. | | Una empresa hija que posees, pero `inactive` | `403 company_inactive` — reactívala primero. | | No es un UUID v7 válido | `400 parameter_invalid_uuid`, con `param: "X-Active-Profile"`. | | Una empresa que **no** posees (otro árbol, o inexistente) | `404 profile_not_found`. | El `404` es **indistinguible** tanto si la empresa pertenece a otro maestro como si no existe — la API nunca revela que una empresa fuera de tu árbol existe: ```json { "error": { "type": "not_found_error", "code": "profile_not_found", "message": "El perfil de empresa indicado no existe o no pertenece a tu cuenta.", "param": "X-Active-Profile" } } ``` El `403` es distinto: la empresa **sí** es tuya, así que revelar que está desactivada es legítimo — es la señal para [reactivarla](/guides/companies#activate) antes de operar: ```json { "error": { "type": "authorization_error", "code": "company_inactive", "message": "Esta empresa está desactivada. Actívala para operar.", "param": "X-Active-Profile" } } ``` ## ¿Cuál deberías usar? [#which] `X-Active-Profile` y las [API keys hijas](/guides/child-api-keys) resuelven necesidades distintas y coexisten: * Usa una **key hija** para entregar una credencial acotada a una integración ligada a una empresa — la credencial en sí queda ligada a esa empresa. * Usa el **header** para manejar muchas empresas desde una sola master key — una credencial, empresa objetivo elegida por petición. El header solo cambia la empresa activa. Nunca cambia los scopes de la key, y el aislamiento entre maestros se aplica igual que al [gestionar las empresas](/guides/companies#scopes): una empresa fuera de tu árbol nunca es observable, devolviendo `404` en vez de `403`. --- # Importes y fechas (/es/guides/amounts-and-dates) Cada valor monetario, de fecha y de hora de la API pública sigue un pequeño conjunto de convenciones fijas. Son las mismas en todos los recursos, así que en cuanto las gestionas en un sitio tu cliente funciona en todas partes. ## Dinero [#dinero] Los importes siempre van en **euros (EUR)** — el campo `currency` está presente en todos los documentos y es `"EUR"` en v1 ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)). Todavía no hay soporte multidivisa. Los importes llevan **dos decimales** (precisión de céntimos). La representación canónica es un **string decimal** con exactamente dos decimales, al estilo Stripe: ```json { "price": "1234.56" } ``` <Callout type="warn"> Algunos recursos emiten actualmente los importes como **números** JSON (floats) en lugar de strings decimales — por ejemplo el `total`, el `subtotal` o el `unit_price` de un documento vuelven como `1802.9`, `968`, `100`. Escribe tu parser para que acepte **tanto** un string como un número en cualquier campo de dinero, y normalízalo a un tipo decimal fijo en tu lado (p. ej. `Decimal` en Python, un big-decimal o un entero de unidades menores en JS). Nunca guardes el dinero como un float binario en crudo. </Callout> ### Deja que la API calcule los totales [#deja-que-la-api-calcule-los-totales] <Callout type="info"> **No redondees ni calcules por adelantado.** Envía los datos en crudo de cada línea (`quantity`, `unit_price`, `discount`, el `*_id` del impuesto) y deja que la API derive el subtotal, el IVA, el recargo, la retención y el total general. El servidor es la única fuente de verdad para cada total — si redondeas los importes de línea por tu cuenta antes de enviarlos, tus cifras pueden divergir de lo que almacena la API. </Callout> El total de un documento sigue una sola fórmula en toda la API: ``` total = subtotal + total_vat + total_surcharge − total_retention ``` Si necesitas previsualizar el desglose **antes** de crear un documento — para un resumen de pedido, un carrito o para conciliar tus propias cifras — llama a `POST /v1/taxes/calculate-totals` con las líneas y lee el `subtotal`, el `total_vat`, el `total_surcharge`, el `total_retention` y el `total` calculados (importes en EUR), más un desglose por línea en el mismo orden: ```json { "subtotal": 250, "total_vat": 52.5, "total_surcharge": 0, "total_retention": 15, "total": 287.5, "lines": [ { "subtotal": 100, "vat_amount": 21, "surcharge_amount": 0, "retention_amount": 0, "total": 121 } ] } ``` El mismo desglose de impuestos por línea aplica a todos los documentos de venta: **facturas, presupuestos, proformas y albaranes** aceptan todos un `retention_rate` y un `surcharge_rate` por línea (retención de IRPF y recargo de equivalencia, 0–100), y su cabecera lleva los `total_vat`, `total_surcharge` y `total_retention` agregados. La misma fórmula se cumple en todas partes. Cada línea además devuelve `retention_rate` y `surcharge_rate` en la respuesta, para que puedas reconciliar el desglose línea a línea. <Callout type="warn"> **El recargo de equivalencia sigue pares legales fijos.** Cuando una línea declara un `surcharge_rate`, debe coincidir con el tipo de IVA de esa línea según el régimen español: **21% → 5.2%**, **10% → 1.4%**, **4% → 0.5%**, **0% → 0%**. Un par ilegal (p. ej. `tax_rate: 21` con `surcharge_rate: 1.4`) se rechaza con `422` y los pares permitidos se devuelven en `error.allowed_values`. Envía solo el recargo que admite el tipo de IVA de la línea. </Callout> ## Facturas de compra — impuesto por línea [#facturas-de-compra--impuesto-por-línea] Una factura de compra registra lo que te cobró un **proveedor**, así que cada una de sus líneas acepta unos calificadores fiscales extra que el lado de venta cubre a su manera. En `CreatePurchaseInvoiceRequest.lines[]` puedes fijar: | Campo | Tipo | Significado | | ------------------ | -------------- | --------------------------------------------------------------------------- | | `retention_rate` | number (0–100) | Retención de IRPF aplicada a la línea. | | `surcharge_rate` | number | Recargo de equivalencia, mismos pares legales que en venta. | | `vat_deductible` | boolean | Informativo — marca el IVA como deducible. **No** cambia el importe pagado. | | `exemption_reason` | enum o `null` | Por qué la línea está exenta o no sujeta (ver abajo). | El total por línea sigue la misma forma que en el lado de venta, restando la retención y sumando el recargo: ``` total de línea = subtotal + impuestos − retention_amount + surcharge_amount ``` El `surcharge_rate` de una línea de compra sigue los **mismos pares legales IVA↔recargo** que una línea de venta, validados en el servidor: **21 → 5.2**, **10 → 1.4**, **4 → 0.5**, **0 → 0**. El par `0 → 0` también es válido en el lado de compra — una línea exenta o a tipo cero lleva un recargo cero. Un par ilegal se rechaza con `422`. `exemption_reason` califica por qué una línea queda fuera del IVA ordinario. Es un enum o `null` (o ausente), lo que significa que la línea hereda la calificación de la cabecera de la factura, o no declara ninguna: | Valor | Clase | | ---------------------------------- | ------------------------------------------ | | `E1`, `E2`, `E3`, `E4`, `E5`, `E6` | Exenta (causa de exención de la LIVA) | | `N1`, `N2` | No sujeta (causa de no sujeción) | | `null` | Heredar de la cabecera / ninguna declarada | Una línea de factura de compra que lleva a la vez un recargo y un motivo de exención: ```json { "description": "Wholesale goods", "quantity": 10, "unit_price": "50.00", "tax_rate": 21, "surcharge_rate": 5.2, "retention_rate": 0, "vat_deductible": true, "exemption_reason": null } ``` ```bash curl -s -X POST https://api.factuarea.com/v1/purchase_invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "supplier_id": "01931b3e-...s01", "received_on": "2026-03-15", "lines": [ { "description": "Exempt service", "quantity": 1, "unit_price": "200.00", "tax_rate": 0, "surcharge_rate": 0, "exemption_reason": "E1" } ] }' | jq '.data | {subtotal, total_vat, total_surcharge, total_retention, total}' ``` <Callout type="info"> **Céntimos en los informes fiscales.** Los endpoints fiscales agregados (Modelo 303 / 347 vía `/v1/tax_reports/*`) devuelven sus importes como **céntimos enteros**, no como decimales de EUR — p. ej. una base imponible acumulada de `25000` significa `250.00 €`. Esto está documentado campo a campo en la spec; trata las cifras de los informes fiscales como unidades menores y divide por 100 solo para mostrarlas. </Callout> ## Fechas [#fechas] Las fechas de calendario (sin componente horario) usan **`YYYY-MM-DD`** — la forma de fecha completa [ISO-8601 / RFC 3339](https://en.wikipedia.org/wiki/ISO_8601). Esto cubre campos como `issued_on`, `due_on`, `paid_on`, `valid_until`, `delivery_date`, `received_on`, `start_on` y `end_on`: ```json { "issued_on": "2026-03-15", "due_on": "2026-04-14", "paid_on": "2026-03-20" } ``` Envía las fechas en el mismo formato. Una fecha no tiene hora ni zona horaria — es el día de calendario tal como queda registrado para el documento. ## Timestamps [#timestamps] Los campos de instante temporal (metadatos de auditoría y ciclo de vida como `created_at`, `updated_at`, `signed_at`, `last_delivery_at`) usan strings de fecha-hora **ISO-8601 / RFC 3339** completos. La mayoría se emiten en **UTC** con un sufijo `Z`: ```json { "created_at": "2026-05-15T10:34:21Z" } ``` Algunos timestamps llevan en su lugar un offset Europe/Madrid explícito (`+01:00` en invierno, `+02:00` en verano): ```json { "created_at": "2026-04-15T10:31:05+02:00" } ``` <Callout type="warn"> Ambas formas son ISO-8601 válidas y denotan el mismo tipo de valor: un instante exacto. **Parsea el offset** — no asumas que el string siempre está en UTC. Un parser ISO-8601 en condiciones (`Instant.parse`, `datetime.fromisoformat`, `new Date(...)`, `Carbon::parse`) gestiona `Z` y `±hh:mm` de forma idéntica y normaliza al instante absoluto. </Callout> ## Zona horaria para las cuotas [#zona-horaria-para-las-cuotas] La cuota **mensual** del rate limit se reinicia el **día 1 de cada mes natural a las `00:00` Europe/Madrid** (CET/CEST), no en UTC. La cuota por minuto es una ventana deslizante y la cabecera `X-RateLimit-Reset` es un **UNIX timestamp** (segundos desde el epoch, independiente de la zona horaria). Consulta [Rate limits](/guides/rate-limits) para la semántica completa de las ventanas. Siempre que la API necesita una única referencia de calendario civil para un límite de negocio — periodos fiscales, el reinicio de la cuota mensual — esa referencia es **Europe/Madrid**. ## Referencia rápida [#referencia-rápida] | Valor | Formato | Ejemplo | | ----------------------------- | --------------------------------------------------------------------- | ------------------------ | | Dinero | EUR, dos decimales — string decimal (algunos campos emiten un número) | `"1234.56"` / `1802.9` | | Divisa | ISO 4217, siempre `EUR` en v1 | `"EUR"` | | Importes de informes fiscales | **Céntimos** enteros (unidades menores) | `25000` → 250.00 € | | Fecha | `YYYY-MM-DD` (ISO-8601 fecha completa) | `"2026-03-15"` | | Timestamp | ISO-8601 fecha-hora, normalmente UTC `Z`, a veces `±hh:mm` | `"2026-05-15T10:34:21Z"` | | Calendario de cuotas / fiscal | Hora civil Europe/Madrid | día 1, `00:00` CET/CEST | --- # Anular o rectificar (/es/guides/annul-vs-correct) Una factura emitida no se puede editar. Todo lo que parece editarla es en realidad una de cuatro operaciones distintas, cada una con sus condiciones previas y su propia consecuencia ante la Administración tributaria. Esta página es la tabla de decisión, y el motivo de cada rama. ## Cuándo aplica [#when] Siempre que algo esté mal en una factura y necesites deshacerlo. **El estado actual de la factura acota las operaciones legales; cuando hay más de una legal, decide tu intención.** La gravedad del error nunca entra en juego. | Estado de la factura | ¿Número asignado? | Operación | Consecuencia | | ---------------------------------------------------------------- | ----------------- | --------------------------------------------------------- | --------------------------------------------------------------------- | | `draft`, y no hay nada que merezca conservarse | No | **Eliminar** — `DELETE /v1/invoices/{id}` | El registro desaparece. Nunca fue fiscal. | | `draft`, pero quieres dejar constancia del intento | No | **Cancelar** — cambiar el estado a `cancelled` | El borrador se retira pero se conserva. | | `sent`, `overdue` — la factura nunca debió emitirse | Sí | **Anular** — `POST /v1/invoices/{id}/annul` | La factura deja de ser cobrable y se declara una anulación a la AEAT. | | `sent`, `paid` — la factura debía existir, su contenido está mal | Sí | **Rectificar** — `POST /v1/invoices/{id}/corrective` | Un documento fiscal nuevo que referencia al original. | | `sent`, pero solo estaba mal la marca de entrega | Sí | **Deshacer la entrega** — `POST /v1/invoices/{id}/unsend` | Se limpia la marca de entrega. La factura sigue emitida. | Tres reglas hacen inequívoca la tabla: **Una factura numerada nunca se elimina físicamente.** Eliminar exige estado `draft` (o un `cancelled` que venga de un borrador) **y** un número que siga siendo el provisional del borrador. Toda factura que consumió un número de su serie queda protegida por el soft-delete fiscal; la vía para retirarla es la anulación ([`BR-INV-002`](#traceability), art. 29.4 de la Ley General Tributaria sobre el deber de conservar los documentos con trascendencia tributaria). **Una factura pagada está cerrada.** `paid` es terminal: su IVA repercutido se ha declarado o se declarará en el periodo y el cobro está identificado, de modo que anularla rompería la trazabilidad y distorsionaría las declaraciones de IVA. El camino canónico es una rectificativa ([`BR-INV-023`](#traceability)). **En `sent` las dos son legales — así que pregúntate qué falló.** Una rectificativa se admite sobre `sent` o `paid` ([`BR-INV-001`](#traceability)) y una anulación sobre `sent` u `overdue` ([`BR-INV-003`](#traceability)): `sent` es el único estado en el que la API acepta cualquiera de las dos. El estado no puede decidir por ti; la pregunta sí: * **La factura nunca debió existir** — se canceló el pedido, fue al cliente equivocado, duplica a otra → **anular**. * **La factura debía emitirse pero su contenido está mal** — importe erróneo, tipo de IVA incorrecto, datos del destinatario mal, una devolución parcial → **rectificativa**. Si anulas por un simple error de importe, declaras una `ANULACION` a la AEAT y quemas el número para nada; la rectificativa era el camino limpio y sigue disponible en `sent`. ## Cancelar no es anular [#cancel] Son actos distintos, y el dominio los mantiene separados a propósito. **Cancelar** retira un *borrador* — un documento que todavía no obliga fiscalmente. Solo está disponible desde `draft`, y `cancelled` es terminal: un borrador cancelado no se puede revivir, se crea uno nuevo ([`BR-INV-012`](#traceability)). **Anular** retira una factura *emitida*. Solo está disponible desde `sent` u `overdue`. No es una eliminación: la factura permanece en el libro registro, en estado `annulled`, y si la empresa está adherida a VeriFactu la anulación se declara a su vez. Intentar cancelar una factura emitida, o anular un borrador, responde `422` con un error de transición inválida. Ese código es deliberado: se trata de una violación de regla de negocio, no de un problema de permisos. ## Las rectificativas no se anulan [#corrective-annul] Una factura rectificativa no se anula nunca. Si la propia rectificativa está mal, emites una **nueva rectificativa de la factura original** ([`BR-INV-003`](#traceability)). Intentarlo responde `422`. El razonamiento es que todo el sentido de una rectificativa es «este documento modifica aquel». Anular la modificación dejaría el original en un estado ambiguo ante la Administración tributaria, donde ambos documentos ya están registrados. ## `unsend` deshace una marca de entrega, no una emisión [#unsend] `unsend` existe para un error concreto: marcar como entregada una factura que no lo estaba. Limpia la marca de entrega y **mantiene el estado en `sent`**. El número de serie, el alta en la AEAT y los snapshots congelados quedan intactos, porque una factura emitida es inmutable ([`BR-INV-030`](#traceability), RD 1007/2023). Dos propiedades importan para las integraciones: * Es **idempotente**. Volver a llamarla cuando la marca ya está limpia es un no-op controlado, nunca un `500`. * Está estrictamente acotada a `sent`. Sobre una factura `paid`, `annulled`, `cancelled` o programada responde `422` — nunca `403`. La matriz de transiciones de estado no contiene ningún camino de vuelta de `sent` a `draft` por ninguna ruta, incluido el endpoint genérico de cambio de estado. No existe la «desemisión». ## Qué envía la API [#api] **Comprueba primero.** [`GET /v1/invoices/{id}/can-annul`](/api-reference/invoices/public-api.v1.invoices.can_annul) (scope `invoices:read`) te da la respuesta antes de comprometerte, incluido si la anulación producirá un registro VeriFactu adicional: ```bash curl https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/can-annul \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ```json { "data": { "can_annul": false, "reasons": ["La factura está pagada."], "will_create_verifactu": false, "info": [] } } ``` **Después anula.** [`POST /v1/invoices/{id}/annul`](/api-reference/invoices/public-api.v1.invoices.annul) (scope `invoices:void`) deja constancia del motivo: ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/annul \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"reason": "El cliente cancela el pedido tras la emisión"}' ``` <Callout type="info"> `POST /v1/invoices/{id}/void` llega a la misma operación de dominio bajo el nombre externo que el contrato de la API usa para el estado (`voided`). Prefiere `annul` cuando quieras dejar constancia del motivo; una segunda llamada sobre una factura ya anulada responde `422` en cualquiera de los dos casos. </Callout> Para el camino de la rectificativa —payload, códigos de rectificación, herencia de líneas— ver [Facturas rectificativas](/guides/corrective-invoices). ## Qué sale en el PDF [#pdf] La anulación no reescribe el documento original. La factura conserva su número, sus datos congelados de destinatario y emisor y su bloque QR; lo que cambia es su estado en el libro registro y el hecho de que ahora existe una segunda declaración ante la AEAT. Eliminar un borrador retira el documento por completo — pero un borrador nunca tuvo número definitivo, ni snapshot congelado, ni QR, que es exactamente por lo que eliminar es seguro ahí y en ningún otro sitio. `unsend` no cambia nada del documento impreso. Solo limpia una marca de entrega; la factura no vuelve a ser editable ([`BR-INV-030`](#traceability)). ## Qué llega a la AEAT [#aeat] **La anulación** de una factura de una empresa adherida a VeriFactu produce un segundo registro de facturación de clase `ANULACION`, encadenado al último registro de la empresa y referido al alta original ([`BR-VFC-014`](#traceability)). Se crea de forma asíncrona, después de que la transacción confirme, así que la factura llega a `annulled` en tu base de datos antes de que se transmita la declaración. Consulta el registro si necesitas confirmar que la AEAT la aceptó — ver [Estados de envío VeriFactu](/guides/verifactu-submission-states). Hay un matiz con consecuencias reales. Si **el alta original nunca se aceptó** —está rechazada, con error, o todavía pendiente—, la anulación debe declarar explícitamente que no existe registro previo en la AEAT. El sistema deriva esa marca del estado del alta en el momento en que se crea la anulación y la persiste como snapshot, de modo que un cambio posterior del estado del original no desincroniza el XML ya transmitido. Sin esa marca, la AEAT rechaza la anulación de plano con «el registro de facturación no existe» ([`BR-VFC-026`](#traceability)). **Cancelar y eliminar un borrador** no llegan a la AEAT de ninguna manera: un borrador nunca se declaró. **Las rectificativas** son documentos fiscales ordinarios y producen su propia alta, exactamente igual que cualquier otra factura. Si la empresa no está adherida a VeriFactu, la anulación funciona igualmente y simplemente no produce declaración. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-INV-001` — una rectificativa exige un original en `sent` o `paid`; ese solape con la anulación en `sent` es la razón de que ahí decida la intención y no el estado. * `BR-INV-002` — soft-delete fiscal: una factura numerada nunca se elimina físicamente. * `BR-INV-003` — la anulación se limita a facturas emitidas; las rectificativas no se anulan nunca. * `BR-INV-012` — la cancelación se limita a borradores y es terminal. * `BR-INV-023` — `paid` es un estado cerrado; se corrige con una rectificativa, nunca con una anulación. * `BR-INV-030` — `unsend` limpia la marca de entrega, mantiene la factura emitida, es idempotente y responde `422` en lugar de `403`. * `BR-VFC-014` — la clase de registro de anulación y la cadena a la que pertenece. * `BR-VFC-026` — la marca de «sin registro previo» en la anulación de un alta que nunca se aceptó. Derivado también de la máquina de estados de factura documentada junto a esas reglas, que es la fuente de verdad de las transiciones citadas en [Cuándo aplica](#when). --- # API keys (autoservicio) (/es/guides/api-keys) Más allá del dashboard de desarrollador, Factuarea expone todo el ciclo de vida de tus API keys desde la API pública v1, para que aprovisiones y rotes credenciales de forma programática. Cinco endpoints bajo `/v1/account/api-keys` cubren listar, crear, recuperar, rotar el secret y revocar — todos limitados a la empresa autenticada. | Operación | Endpoint | Scope | | ----------------- | --------------------------------------------------- | --------------- | | Listar keys | `GET /v1/account/api-keys` | `account:read` | | Crear una key | `POST /v1/account/api-keys` | `account:write` | | Recuperar una key | `GET /v1/account/api-keys/{api_key}` | `account:read` | | Rotar el secret | `POST /v1/account/api-keys/{api_key}/rotate_secret` | `account:write` | | Revocar una key | `POST /v1/account/api-keys/{api_key}/revoke` | `account:write` | `{api_key}` es el `id` de la key — un UUID v7 opaco, **no** su prefix ni su secret. Mira los esquemas completos en la [Referencia de la API](/api-reference/account/public-api.v1.account.api_keys.list). ## Lista tus keys [#lista-tus-keys] `GET /v1/account/api-keys` devuelve tus keys con [paginación por cursor](/guides/pagination). Cada key expone su `prefix`, `scopes`, `tier`, `environment` y los timestamps de su ciclo de vida — nunca el secret. ```bash curl https://api.factuarea.com/v1/account/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ```json { "data": [ { "object": "api_key", "id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f", "name": "Production sync", "prefix": "fact_live_8KqW3pXn", "scopes": ["invoices:read", "invoices:write"], "tier": "scale", "environment": "live", "active": true, "revoked": false, "last_used_at": "2026-06-23T18:04:11Z", "expires_at": null, "revoked_at": null } ], "has_more": false, "next_cursor": null } ``` `prefix` son los primeros caracteres de la key — seguro de registrar en logs, **no** autentica. Úsalo para reconocer una key en tus propios paneles sin guardar nunca el secret. ## Crea una key [#create] `POST /v1/account/api-keys` emite una nueva key y devuelve su `secret` en texto plano **exactamente una vez**. Guárdalo en el momento en que lo recibes — no hay ningún endpoint para volver a leerlo más tarde. ```bash curl -X POST https://api.factuarea.com/v1/account/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Reporting export", "scopes": ["invoices:read", "pdfs:read"], "environment": "test" }' ``` Respuesta (`201`): ```json { "data": { "object": "api_key", "id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162", "name": "Reporting export", "prefix": "fact_test_1N0Fnyhh", "secret": "fact_test_1N0FnyhhR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "pdfs:read"], "tier": "scale", "environment": "test" } } ``` <Callout type="warn"> El campo `secret` aparece **solo** en esta respuesta `201` (y tras una rotación). Nunca lo devuelve el listado, la recuperación ni ningún otro endpoint. Si lo pierdes tienes que rotar la key. Persístelo en un gestor de secretos de inmediato — nunca en un log ni en un repositorio. </Callout> ### Cuerpo de la petición [#cuerpo-de-la-petición] | Campo | Obligatorio | Notas | | ------------- | ----------- | ----------------------------------------------------------------------------------- | | `name` | sí | Etiqueta legible (1–120 caracteres). | | `scopes` | sí | Uno o más [scopes](/guides/authentication#scopes) del catálogo cerrado. Mínimo uno. | | `environment` | no | `live` (por defecto) o `test`. Ver [abajo](#environment). | | `expires_at` | no | Instante futuro ISO 8601 a partir del cual la key deja de autenticar. | | `allowed_ips` | no | Lista opcional de IPs / CIDR permitidas (IPv4, IPv6, `/N`). | El `tier` se **deriva del plan de tu empresa** (o de un [boost de capacidad](/guides/rate-limits#capacity-boost) activo cuando es superior) — *no* se fija desde el cuerpo. Si envías un `tier`, se ignora. Pedir un scope fuera del catálogo cerrado, o un scope por encima de tu plan, devuelve `422` con [errores por campo](/guides/errors). ## El campo environment [#environment] Cada key pertenece a uno de los dos environments, fijado al crearla y visible en el objeto de la key: | `environment` | Prefix | Opera sobre | | ------------- | ------------ | ------------------------------------------------------------------------- | | `live` | `fact_live_` | Tu empresa real, con efectos reales (VeriFactu → AEAT, emails, webhooks). | | `test` | `fact_test_` | Una empresa sandbox aislada con los efectos externos desactivados. | Pasa `environment: test` al crear una key para emitir una credencial de sandbox; omítelo para una key de producción. El prefix refleja el environment, así que los distingues sin decodificar la key. Mira [Modo de prueba y sandbox](/guides/test-mode) para saber qué se desactiva en `test`. ## Rota el secret [#rotate] `POST /v1/account/api-keys/{api_key}/rotate_secret` genera un nuevo `prefix` + `secret` y devuelve el nuevo secret en texto plano **exactamente una vez**. El secret **anterior** sigue funcionando durante una **ventana de gracia de 24 horas** para que puedas desplegar el nuevo sin downtime. ```bash curl -X POST \ https://api.factuarea.com/v1/account/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/rotate_secret \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ```json { "data": { "object": "api_key", "id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f", "prefix": "fact_live_Zq7mP4xV", "secret": "fact_live_Zq7mP4xVnR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "invoices:write"], "environment": "live" } } ``` <Callout type="warn"> Durante la ventana de gracia de 24 horas autentican tanto el nuevo como el secret anterior; una petición que siga usando el anterior recibe un header `199` `Warning` con la cuenta atrás de horas restantes. Al expirar la ventana el secret anterior se rechaza y se purga. Despliega el nuevo secret dentro de esas 24 horas. La rotación es irreversible. </Callout> ## Revoca una key [#revoke] `POST /v1/account/api-keys/{api_key}/revoke` invalida una key de forma permanente. Las peticiones posteriores autenticadas con ella fallan con `401`. Un `reason` opcional (máx 500 caracteres) queda en el audit log. ```bash curl -X POST \ https://api.factuarea.com/v1/account/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/revoke \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -d '{"reason": "Rotated out of the deploy pipeline"}' ``` <Callout type="warn"> La revocación es **irreversible** y **no** se limita a otras keys: puedes revocar la propia key con la que estás autenticando la petición, cortando tu propio acceso. Asegúrate de tener otra key válida en su sitio antes si todavía necesitas acceso a la API. </Callout> Tras la revocación, las peticiones con esa key devuelven `401` con el código **genérico** `invalid_api_key` — no un código específico de "revocada": ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "La API key proporcionada no es válida.", "request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV" } } ``` Esto es **anti-enumeración** deliberada: la API nunca revela si una key fue revocada, ha caducado o nunca existió — toda key inutilizable se ve igual para un atacante. Ramifica tu propia lógica según el resultado `200`/`401`, no según un código específico de revocada. ## Scopes y aislamiento [#scopes-y-aislamiento] Los cinco endpoints están protegidos por los scopes de `account`: * `account:read` — listar y recuperar keys. * `account:write` — crear, rotar y revocar keys. Todas las operaciones están limitadas a la empresa autenticada. Un `id` de key que pertenece a otra empresa devuelve `404 api_key_not_found` (de nuevo, anti-enumeración — nunca revela que la key existe), nunca `403`. <Callout type="info"> Gestionar keys sigue requiriendo una key existente con los scopes adecuados. Crea tu **primera** key en el dashboard de desarrollador ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)), y luego usa estos endpoints para aprovisionar el resto de forma programática. Mira [Autenticación](/guides/authentication) para el formato de la key y las cabeceras. </Callout> --- # Autenticación (/es/guides/authentication) La API de Factuarea autentica cada request con una **API key**. Las claves son tokens opacos generados en el dashboard de desarrolladores ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)) y vinculados a una empresa concreta. Cada request a `https://api.factuarea.com/v1/*` debe incluir una clave válida en uno de los dos formatos soportados. ## Formato de la API key [#formato-de-la-api-key] ``` fact_live_<24 alphanumeric characters> fact_test_<24 alphanumeric characters> ``` Ejemplo: ``` fact_live_8KqW3pXnR2VbY7TcA9eFmN5z fact_test_3pXnR2VbY7TcA9eFmN5z8KqW ``` * **Prefijo**: determina el **entorno**. `fact_live_` opera sobre tu empresa real (producción); `fact_test_` opera sobre una empresa sandbox aislada con los efectos externos (VeriFactu → AEAT, FACe, emails, webhooks) desactivados. El prefijo te permite identificar el entorno sin decodificar la clave. Consulta [Modo de prueba y sandbox](/guides/test-mode). * **Secreto**: 24 caracteres base62 → \~143 bits de entropía. Se muestra **solo una vez** al crearla en el dashboard. Si la pierdes, debes rotarla. * **Hash en BD**: el backend solo almacena el hash bcrypt cost-12 del secreto. No hay forma de recuperarlo. <Callout type="info"> Todos los ejemplos de esta guía usan una clave `fact_live_`, pero el mismo request funciona con una clave `fact_test_` — basta con cambiar el prefijo para operar sobre datos de sandbox. Crea y valida tu integración primero en test. Consulta [Modo de prueba y sandbox](/guides/test-mode). </Callout> ## Enviar la clave en cada request [#enviar-la-clave-en-cada-request] La API acepta dos formatos equivalentes. Elige el que mejor encaje con tu cliente: ### Authorization Bearer (recomendado) [#authorization-bearer-recomendado] ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ### Cabecera X-API-Key [#cabecera-x-api-key] ```bash curl https://api.factuarea.com/v1/clients \ -H "X-API-Key: fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Envía solo una de las dos cabeceras. Si ambas están presentes, la cabecera `Authorization: Bearer` tiene prioridad. ## Ejemplos por lenguaje [#ejemplos-por-lenguaje] <Tabs items="['PHP (Guzzle)', 'Node.js (fetch)', 'Python (requests)']"> <Tab value="PHP (Guzzle)"> ```php $client = new GuzzleHttp\Client([ 'base_uri' => 'https://api.factuarea.com/v1/', 'headers' => [ 'Authorization' => 'Bearer ' . getenv('FACTUAREA_API_KEY'), 'Accept' => 'application/json', ], ]); $response = $client->get('clients?limit=10'); $body = json_decode((string) $response->getBody(), true); ``` </Tab> <Tab value="Node.js (fetch)"> ```javascript const res = await fetch('https://api.factuarea.com/v1/clients?limit=10', { headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, Accept: 'application/json', }, }); const data = await res.json(); ``` </Tab> <Tab value="Python (requests)"> ```python import os import requests resp = requests.get( 'https://api.factuarea.com/v1/clients', params={'limit': 10}, headers={ 'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}", 'Accept': 'application/json', }, ) resp.raise_for_status() data = resp.json() ``` </Tab> </Tabs> ## OAuth 2.1 [#oauth] Para integraciones de agente y apps de terceros que actúan en nombre de un usuario de Factuarea, la API también admite el **flujo OAuth 2.1 authorization-code con PKCE** (`code_challenge_method=S256`) como alternativa a una API key estática. Los mismos [scopes](#scopes) protegen el access token, y la [política de rotación](#rotation-policy) aplica también a los secretos de cliente OAuth. Los metadatos de discovery (RFC 8414) se publican en `/.well-known/oauth-authorization-server`, para que los clientes OAuth resuelvan los endpoints de autorización y token automáticamente: ```bash curl https://api.factuarea.com/.well-known/oauth-authorization-server ``` El esquema de seguridad `OAuth2` — incluidas las URL de autorización y token y la lista completa de scopes — se describe en la [Referencia de la API](/api-reference). ## Scopes [#scopes] Cada API key se crea con uno o más **scopes** que limitan qué endpoints puede invocar. Los scopes son cadenas con la forma `<resource>:<action>`. El catálogo es **cerrado**: cualquier scope fuera del conjunto listado provoca `invalid_scope` al crear la clave. ### Clientes y catálogo [#clientes-y-catálogo] | Scope | Permite | | ------------------ | ------------------------------- | | `clients:read` | Listar y consultar clientes. | | `clients:write` | Crear y actualizar clientes. | | `clients:delete` | Eliminar clientes. | | `products:read` | Listar y consultar productos. | | `products:write` | Crear y actualizar productos. | | `products:delete` | Eliminar productos. | | `suppliers:read` | Listar y consultar proveedores. | | `suppliers:write` | Crear y actualizar proveedores. | | `suppliers:delete` | Eliminar proveedores. | ### Documentos de venta [#documentos-de-venta] | Scope | Permite | | ---------------------------- | --------------------------------------------------------------- | | `invoices:read` | Listar y consultar facturas. | | `invoices:write` | Crear y actualizar facturas (incluye duplicar y rectificativa). | | `invoices:delete` | Eliminar borradores de factura. | | `invoices:send` | Enviar factura por email al cliente. | | `invoices:void` | Anular una factura emitida. | | `quotes:read` | Listar y consultar presupuestos. | | `quotes:write` | Crear y actualizar presupuestos. | | `quotes:delete` | Eliminar presupuestos. | | `quotes:send` | Enviar presupuesto por email. | | `quotes:transition` | Aceptar, rechazar o convertir presupuestos. | | `proformas:read` | Listar y consultar facturas proforma. | | `proformas:write` | Crear y actualizar facturas proforma. | | `proformas:delete` | Eliminar facturas proforma. | | `proformas:send` | Enviar factura proforma por email. | | `proformas:transition` | Convertir factura proforma en factura. | | `delivery_notes:read` | Listar y consultar albaranes. | | `delivery_notes:write` | Crear y actualizar albaranes. | | `delivery_notes:delete` | Eliminar albaranes. | | `delivery_notes:transition` | Marcar como entregado/cancelado, firmar, convertir. | | `delivery_notes:gdpr_forget` | Borrar la PII de auditoría de firma (RGPD Art. 17). | ### Compras y recurrentes [#compras-y-recurrentes] | Scope | Permite | | ------------------------------- | -------------------------------------------- | | `purchase_invoices:read` | Listar y consultar facturas de compra. | | `purchase_invoices:write` | Crear y actualizar facturas de compra. | | `purchase_invoices:delete` | Eliminar facturas de compra. | | `purchase_invoices:transition` | Marcar como pagada, recibida, contabilizada. | | `recurring_invoices:read` | Listar y consultar plantillas recurrentes. | | `recurring_invoices:write` | Crear y actualizar plantillas recurrentes. | | `recurring_invoices:delete` | Eliminar plantillas recurrentes. | | `recurring_invoices:transition` | Pausar, reanudar y emitir manualmente. | ### Catálogos y exportación [#catálogos-y-exportación] | Scope | Permite | | ------------------- | ---------------------------------------------------------------------------------------------------------------- | | `taxes:read` | Leer el catálogo (global) de tipos impositivos. | | `taxes:write` | Crear y actualizar tipos impositivos. | | `taxes:delete` | Eliminar tipos impositivos. | | `series:read` | Listar series de numeración de facturas. | | `series:write` | Crear y actualizar series de numeración de facturas. | | `pdfs:read` | Descargar PDFs de cualquier documento con el scope `:read` correspondiente. | | `tax_reports:read` | Leer informes fiscales (Modelo 303/347, etc.). | | `tax_reports:write` | Generar informes fiscales. | | `account:read` | Leer la cuenta autenticada (`GET /v1/account`). | | `account:write` | Gestionar las API keys de la propia cuenta (crear, rotar, revocar) y actualizar la personalización de la cuenta. | ### VeriFactu y FacturaE [#verifactu-y-facturae] | Scope | Permite | | ----------------- | ------------------------------------------------------------------- | | `verifactu:read` | Leer registros, eventos, certificados y configuración de VeriFactu. | | `verifactu:write` | Gestionar certificados, ajustes y reintentos de VeriFactu. | | `facturae:read` | Descargar el XML FacturaE de una factura y leer sus envíos a FACe. | | `facturae:write` | Enviar facturas a FACe y solicitar la anulación de envíos. | ### Webhooks y eventos [#webhooks-y-eventos] | Scope | Permite | | ----------------- | ---------------------------------------------------------- | | `webhooks:read` | Listar webhook endpoints y entregas. | | `webhooks:write` | Crear, actualizar, rotar y hacer ping a webhook endpoints. | | `webhooks:delete` | Eliminar webhook endpoints. | | `events:read` | Leer el catálogo de eventos y eventos individuales. | ### Empresas gestionadas (gestoría) [#empresas-gestionadas-gestoría] Scopes detallados para el modelo de **gestoría**, donde una cuenta maestra gestiona empresas hijas y sus API keys. Solo accesibles con API key (sin equivalente en el consentimiento OAuth); `companies:*` requiere además el módulo del plan de gestoría. | Scope | Permite | | ------------------ | ------------------------------------------------------------------ | | `companies:read` | Listar y consultar las empresas gestionadas (sub-cuentas hijas). | | `companies:write` | Crear, actualizar, activar y desactivar empresas gestionadas. | | `companies:delete` | Archivar empresas gestionadas. | | `api_keys:read` | Listar y consultar las API keys de las empresas gestionadas. | | `api_keys:write` | Crear, rotar y revocar las API keys de las empresas gestionadas. | | `api_keys:delete` | Eliminar permanentemente las API keys de las empresas gestionadas. | ### Super-scope [#super-scope] | Scope | Permite | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `*` | Acceso total — equivalente a tener todos los demás scopes anteriores. Reservado para claves de owner / migraciones puntuales. **Evita usarlo en integraciones de producción**. | Si un request usa un endpoint que requiere un scope no concedido a la clave, la respuesta es `403` con `type: authorization_error` y `code: insufficient_scope`. ```json { "error": { "type": "authorization_error", "code": "insufficient_scope", "message": "La API key no tiene el scope requerido para esta operación.", "request_id": "req_01JBVH7..." } } ``` ## Gestión de claves [#gestión-de-claves] Las API keys se gestionan desde el dashboard de desarrolladores ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)), no a través de la API pública. Desde ahí puedes crear claves, rotar su secreto, revocarlas, configurar scopes, un `expires_at` opcional y una lista de acceso por IP. Los metadatos de la clave autenticada (id, name, prefix, scopes, tier, `last_used_at`, `expires_at`) se pueden leer vía `GET /v1/account` — pero el secreto **nunca** se devuelve. <Callout type="warn"> **No hay ningún endpoint para "ver" el secreto**. Solo se muestra una vez al crearlo. Si pierdes el valor debes rotar la clave en el dashboard y volver a desplegar el nuevo secreto. Es deliberado: minimiza la ventana de exposición. </Callout> ### Política de rotación [#rotation-policy] Las API keys y los secretos de cliente OAuth son credenciales de larga vida y deben rotarse en un calendario y de inmediato tras cualquier sospecha de filtración. * Los **prefijos** son la fuente de verdad del entorno: `fact_live_` (producción) y `fact_test_` (sandbox). Nunca los mezcles entre entornos. * **Rota** desde el dashboard (o mediante los [endpoints self-service `account:write`](/guides/api-keys#rotate)) para emitir un secreto nuevo. El nuevo secreto se devuelve **una sola vez** — guárdalo de inmediato, no se vuelve a mostrar. * **Ventana de gracia (doble secreto).** Tras una rotación el secreto anterior sigue funcionando durante una **ventana de gracia de 24 horas**, para que puedas desplegar el nuevo secreto sin tiempo de inactividad. Durante esa ventana se aceptan tanto el nuevo como el anterior; al expirar la ventana el secreto anterior se rechaza y se purga. Un request que siga usando el secreto anterior recibe un header `199` `Warning` que indica cuántas horas quedan antes de que deje de funcionar. * **Cuándo rotar**: en un calendario regular (p. ej. cada 90 días), siempre que un miembro del equipo con acceso se vaya, e **inmediatamente** si un secreto queda expuesto alguna vez en logs, control de versiones o un cliente público. * **Revoca** para invalidar una clave permanentemente. Cualquier request posterior con ella falla con `401`. La revocación no tiene ventana de gracia — es instantánea e irreversible. Los secretos están ligados a una sola empresa (tenant) y nunca deben incrustarse en navegadores, apps móviles ni ningún cliente público — mantenlos solo en el servidor. ### Lista de acceso por IP [#lista-de-acceso-por-ip] Cada API key se puede restringir a una lista de IPs o rangos CIDR desde el dashboard. Si el request llega desde una IP fuera de la lista de acceso, la respuesta es `401` y el incidente se registra en el log de auditoría. Deja la lista de acceso vacía para permitir cualquier IP. ## Errores de autenticación [#errores-de-autenticación] Los fallos relacionados con la API key responden con HTTP `401` (o `403` para `insufficient_scope`) y el envoltorio de error estándar. El campo `code` distingue el caso: | `code` | HTTP | Causa | | ------------------------ | ----- | ------------------------------------------------------------------------------------------------ | | `missing_api_key` | `401` | No se ha enviado ninguna cabecera de autenticación. | | `invalid_api_key` | `401` | La clave no existe, tiene un formato incorrecto o el secreto no coincide con el hash almacenado. | | `api_key_revoked` | `401` | La clave fue revocada o ha expirado. Crea una nueva en el dashboard. | | `too_many_auth_failures` | `429` | Demasiados intentos de autenticación fallidos; espera antes de reintentar. | | `insufficient_scope` | `403` | La clave carece del scope que requiere el endpoint. | Cada respuesta incluye un `request_id` único (también en la cabecera `X-Request-Id`) que puedes facilitar a soporte al investigar. ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "La API key proporcionada no es válida.", "request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV", "doc_url": "https://docs.factuarea.com/guides/errors#invalid_api_key" } } ``` ## Buenas prácticas [#buenas-prácticas] * **Nunca** subas API keys a repositorios — usa variables de entorno o un gestor de secretos (AWS Secrets Manager, Doppler, 1Password Service Accounts). * Crea **una clave por integración**: facilita rotar y auditar el acceso sin afectar al resto. * Limita los scopes al mínimo necesario. Un script de exportación solo necesita scopes `:read` concretos. * Activa la lista de acceso por IP para integraciones servidor a servidor con IPs estables. * Configura `expires_at` para claves temporales (p. ej. consultorías, demos). * Audita el uso desde el dashboard: `Developers > API Keys > Activity` muestra IPs, rutas y errores por clave. --- # Operaciones en lote (/es/guides/bulk-operations) Los endpoints bulk procesan varias filas en una sola petición y **nunca hacen fallar todo el lote porque una fila se rechace**. Cada fila se evalúa de forma independiente y la respuesta informa, fila a fila, de si se aplicó o no. Es el contrato de **éxito parcial** (partial-success), compartido por todos los endpoints bulk de la API pública. La superficie bulk ahora abarca operaciones de **delete, create, pdf, send y status** sobre los recursos de documento y de catálogo — no solo la familia original `bulk-delete`. Algunas devuelven la forma `BulkPartialSuccessResult` de abajo, `bulk-create` devuelve la forma más rica `BulkCreateResult`, y `bulk-pdf` transmite un ZIP binario en lugar del envoltorio JSON. Todas honran el éxito parcial: una fila mala nunca hunde el lote. ## Forma de la respuesta [#forma-de-la-respuesta] Una operación bulk siempre devuelve `200 OK` con un `BulkPartialSuccessResult` dentro de `data`: ```json { "data": { "total": 3, "successful": 2, "failed": 1, "failures": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "error_code": "resource_not_deletable", "error_message": "La factura ya está emitida y no se puede eliminar." } ] } } ``` | Campo | Tipo | Significado | | ------------ | ------- | ------------------------------------------------------------------------------------------------- | | `total` | integer | Filas procesadas (`successful + failed`). | | `successful` | integer | Filas aplicadas (eliminadas, creadas o validadas). | | `failed` | integer | Filas que no se pudieron procesar. Igual a la longitud de `failures`. | | `failures` | array | Un elemento por cada fila fallida. Siempre una lista — vacía, nunca `null`, cuando no falló nada. | El invariante `total === successful + failed` y `failed === failures.length` se cumple siempre. Un lote totalmente correcto devuelve `failures: []`. ## Un elemento de fallo [#un-elemento-de-fallo] Cada entrada de `failures` identifica la fila y explica por qué se rechazó. La identidad es **polimórfica**: * `id` — el UUID de un recurso existente (bulk-delete y otras operaciones sobre recursos existentes). * `index` — la posición (base 0) de la fila en el lote, para filas nuevas que aún no tienen recurso (bulk-create con `dry_run`, import CSV). Exactamente uno de `id` / `index` está presente. | Campo | Tipo | Significado | | --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | UUID v7 del recurso existente que no se pudo procesar. | | `index` | integer | Posición (base 0) de la fila dentro del lote. | | `error_code` | string | Código legible por máquina del catálogo de errores v1 (estable entre idiomas). Ramifica según esto. | | `error_message` | string | Motivo legible por humanos, en español. Para mostrar, no para ramificar. | | `errors` | array | Problemas bloqueantes por campo (`FieldIssue[]`). Presentes en flujos `validate-only` / `bulk-create`; ausentes en `bulk-delete`. | | `warnings` | array | Avisos no bloqueantes por campo (`FieldIssue[]`). | <Callout type="info"> Ramifica según **`error_code`**, nunca según `error_message` — el mensaje es texto en español orientado a personas y puede cambiar. Para bulk-delete los códigos son `resource_not_found` (el UUID no existe o pertenece a otra empresa) y `resource_not_deletable` (el recurso existe pero su estado impide eliminarlo: un albarán firmado, un presupuesto facturado, un cliente con documentos, etc.). </Callout> ## Leer el resultado [#leer-el-resultado] No trates la llamada como todo-o-nada. Inspecciona `failures` y actúa fila a fila: <Tabs items="['cURL', 'TypeScript', 'Python']"> <Tab value="cURL"> ```bash curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-delete \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02", "01931b3e-...a03"] }' \ | jq '.data | {total, successful, failed, failures}' ``` </Tab> <Tab value="TypeScript"> ```ts const { data } = await factuarea.quotes.bulkDelete({ ids }); if (data.failed > 0) { for (const f of data.failures) { // f.id, f.error_code, f.error_message console.warn(`${f.id} → ${f.error_code}: ${f.error_message}`); } } ``` </Tab> <Tab value="Python"> ```python res = factuarea.quotes.bulk_delete(ids=ids) data = res["data"] for f in data["failures"]: # ramifica según error_code, muestra error_message print(f["id"], f["error_code"], f["error_message"]) ``` </Tab> </Tabs> ## UUID ajenos y desconocidos [#uuid-ajenos-y-desconocidos] Los UUID que no pertenecen a tu empresa, o que no existen, **nunca son un 404 global**. El handler filtra por `company_id`, así que un UUID ajeno o desconocido se reporta como un fallo normal (`resource_not_found`) — nunca revela si un recurso existe en otro tenant. ## Bulk create (solo validar con dry\_run) [#bulk-create-solo-validar-con-dry_run] `bulk-create` acepta hasta **100** filas para facturas y hasta **500** para clientes en una sola llamada, y devuelve un envoltorio más rico, `BulkCreateResult`. El flag `dry_run` (por defecto `false`) alterna entre dos comportamientos: * **`dry_run: true`** valida cada fila **sin persistir nada** y devuelve un `results[]` por fila. Cada entrada lleva su `index`, un `status`, y los `errors[]` / `warnings[]` encontrados para esa fila. No se escribe nada — úsalo para mostrar los problemas en tu interfaz antes de confirmar. * **`dry_run: false`** crea **solo las filas válidas**. Las filas que fallan la validación no se crean y vuelven en `failures[]`, cada una identificada por su `index` (base 0). El envoltorio lleva ambos arrays, así que el mismo parser sirve en cualquier modo: ```json { "data": { "dry_run": true, "total": 2, "successful": 1, "failed": 1, "results": [ { "index": 0, "status": "valid", "errors": [], "warnings": [] }, { "index": 1, "status": "invalid", "errors": [{ "field": "client_id", "code": "required", "message": "El cliente es obligatorio." }], "warnings": [] } ], "failures": [ { "index": 1, "error_code": "validation_failed", "error_message": "Faltan campos obligatorios en la fila." } ] } } ``` Valida primero con `dry_run: true`, corrige lo que marque `results[]` y vuelve a enviar el mismo payload con `dry_run: false` para persistir las filas que pasan: ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-create \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9f1c2b7a-0e44-4c1a-8f3d-1a2b3c4d5e6f" \ -d '{ "dry_run": true, "invoices": [ { "client_id": "01931b3e-...c01", "lines": [{ "description": "Consulting", "quantity": 1, "unit_price": "100.00", "tax_rate_id": "01931b3e-...t21" }] }, { "lines": [{ "description": "Missing client", "quantity": 1, "unit_price": "50.00" }] } ] }' | jq '.data | {dry_run, total, successful, failed, results, failures}' ``` ## Bulk PDF (descarga ZIP) [#bulk-pdf-descarga-zip] `bulk-pdf` empaqueta los PDF de hasta **50** documentos en un único ZIP y transmite de vuelta el **archivo binario** — **no** devuelve el envoltorio JSON. Los id que no se encuentran, o que no tienen PDF disponible, **no** abortan la petición: el ZIP lleva solo los documentos válidos, y los recuentos por id viajan en cabeceras de respuesta `X-Bulk-*` para que puedas conciliar qué entró. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-pdf \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"] }' \ -D - -o invoices.zip ``` El flag `-D -` vuelca las cabeceras de respuesta: lee `X-Bulk-Total`, `X-Bulk-Successful` y `X-Bulk-Failed` para saber cuántos id entraron en el archivo. ## Bulk send (envío en lote) [#bulk-send-envío-en-lote] `bulk-send` encola hasta **200** documentos para enviarlos por email y devuelve la forma `BulkPartialSuccessResult`. El envío es asíncrono: una fila `successful` significa que el email se **encoló**, no que ya se entregó. Los campos opcionales `to`, `cc`, `subject`, `message` y `language` sobrescriben los valores por defecto para todo el lote. ```bash curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-send \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c3e1f90-2a11-4b22-9d44-5e6f7a8b9c0d" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "language": "es" }' \ | jq '.data | {total, successful, failed, failures}' ``` ## Transiciones de estado en lote [#transiciones-de-estado-en-lote] `bulk-status` mueve hasta **50** documentos a un nuevo estado, y cada transición pasa **por la guarda del Aggregate** — una fila cuyo estado actual prohíbe el movimiento falla de forma individual y cae en `failures[]`, mientras el resto sí transiciona. El `new_status` destino debe pertenecer al conjunto cerrado permitido para ese recurso (consulta la tabla de abajo). Para facturas, `payment_date` es **obligatorio** cuando `new_status` es `paid`. Para facturas de compra, `payment_date` también es obligatorio y se propaga **tal cual** — nunca se reemplaza en silencio por `now()`. Para productos y proveedores la transición es **idempotente**: un recurso que ya está en el estado solicitado cuenta como `successful` sin cambiar nada. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-status \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3b8d2e10-4f55-4c66-8a77-9b0c1d2e3f40" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "new_status": "paid", "payment_date": "2026-03-20" }' \ | jq '.data | {total, successful, failed, failures}' ``` Los valores `new_status` permitidos por recurso: | Recurso | `new_status` permitido | | ------------------- | ---------------------------------- | | `invoices` | `sent`, `paid` | | `quotes` | `approved`, `rejected` | | `proformas` | `accepted`, `rejected` | | `delivery_notes` | `delivered`, `cancelled` | | `purchase_invoices` | `paid` | | `products` | `active`, `inactive` (idempotente) | | `suppliers` | `active`, `inactive` (idempotente) | ## Endpoints y límites [#endpoints-y-límites] Cada operación limita el lote a un número fijo de filas. Trocear un trabajo mayor en bloques dentro de estos límites queda de tu parte: | Operación | Recursos | Filas máx. | Forma de respuesta | | ------------- | ------------------------------------------------------------------------------------------------- | ---------- | -------------------------- | | `bulk-create` | `invoices` (100), `clients` (500) | 100 / 500 | `BulkCreateResult` | | `bulk-pdf` | `invoices`, `quotes`, `proformas`, `delivery_notes` | 50 | ZIP binario + `X-Bulk-*` | | `bulk-send` | `invoices`, `quotes`, `proformas`, `delivery_notes` | 200 | `BulkPartialSuccessResult` | | `bulk-status` | `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices`, `products`, `suppliers` | 50 | `BulkPartialSuccessResult` | | `bulk-delete` | los nueve recursos | — | `BulkPartialSuccessResult` | <Callout type="info"> **Usa `Idempotency-Key` en las operaciones bulk que mutan.** `bulk-create`, `bulk-send`, `bulk-status` y `bulk-delete` aceptan todas la cabecera `Idempotency-Key`, así que un reintento tras una conexión caída repite el resultado original en lugar de ejecutar el lote dos veces. `bulk-pdf` es una lectura pura y no necesita clave. </Callout> ## Versionado — la forma legacy [#versionado--la-forma-legacy] La forma de éxito parcial es el contrato actual. Los integradores **anclados a una versión anterior a `2026-09-01`** (mediante el header `Factuarea-Version` o un pin en la API key) siguen recibiendo la forma anterior de bulk-delete, de modo que ninguna integración existente se rompe: ```json { "object": "bulk_delete_result", "deleted": 2, "failed": [ { "id": "01931b3e-...a01", "reason": "La factura ya está emitida y no se puede eliminar." } ] } ``` El mapeo entre ambas formas es mecánico: `deleted` es el nuevo `successful`, y cada `failed[].reason` legacy es el nuevo `failures[].error_message` (la nueva forma añade encima el `error_code` estable y el contador `total`). No envíes header —o envía una fecha igual o posterior a `2026-09-01`— para obtener la forma de éxito parcial. <Callout type="warn"> Ancla una versión solo para congelar un contrato del que ya dependes. Las integraciones nuevas deberían usar la forma de éxito parcial: lleva un `error_code` estable e independiente del idioma por el que ramificar, cosa que la cadena `reason` legacy no ofrece. </Callout> --- # Verificación censal AEAT (/es/guides/census-verification) Factuarea puede comprobar que el par **razón social + NIF** de tu empresa está correctamente identificado en el **censo de la AEAT** — la misma identificación que realiza la AEAT cuando recibe tus registros de facturación VeriFactu. Un par no censado provoca el rechazo del envío (error AEAT 4104, *titular no identificado*), así que verificar **pronto** — justo después del registro y cada vez que cambien tus datos fiscales — te ahorra envíos rechazados después. ``` POST /v1/account/census-verification ``` * **Scope:** `account:read` * **Body de la request:** ninguno — la comprobación se ejecuta siempre contra la razón social y el `tax_id` **persistidos** de la cuenta autenticada. Nunca acepta un NIF o nombre arbitrario en el payload. * **Efecto:** el resultado se guarda como snapshot en tu empresa (visible en los ajustes de la app de Factuarea). Cambiar la razón social o el `tax_id` resetea el snapshot hasta que vuelvas a verificar. <Callout type="info"> Esta es la misma verificación que Factuarea ofrece en la app durante el onboarding. Los resultados negativos son **informativos**: nunca bloquean el registro, la facturación ni ninguna otra operación — solo te avisan de que los envíos VeriFactu pueden ser rechazados hasta que corrijas los datos censales. </Callout> ## Llamar al endpoint [#llamar-al-endpoint] ```bash curl -X POST https://api.factuarea.com/v1/account/census-verification \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` Respuesta (`200`): ```json { "data": { "object": "census_verification", "status": "identified", "verified_name": "ACME SOLUTIONS SL", "checked_at": "2026-06-10T22:15:04+00:00" } } ``` `verified_name` es la razón social que se contrastó contra el censo (la persistida). `checked_at` es el momento de la verificación, en ISO 8601. Con los SDK oficiales: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); const result = await factuarea.account.verifyCensus(); // result.data.status → "identified" | "not_identified" | … ``` </Tab> <Tab value="PHP"> ```php <?php use Factuarea\Sdk\Custom\FactuareaClient; $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); $response = $factuarea->account->publicApiV1AccountVerifyCensus(); ``` </Tab> </Tabs> ## Verifica también a tus clientes [#clients] La AEAT ejecuta la misma identificación sobre el **destinatario** de cada registro de facturación VeriFactu: un cliente cuyo par nombre + NIF no está censado provoca el rechazo del envío con el error AEAT **1239** (*destinatario no identificado*). Factuarea **no valida deliberadamente a los clientes contra el censo al crearlos** — y es por diseño, no un descuido: los clientes extranjeros no tienen entrada en el censo español, las empresas recién constituidas pueden tardar días en aparecer, las facturas simplificadas B2C no llevan NIF de destinatario, y el propio servicio de la AEAT puede estar caído (toda la funcionalidad es fail-open). Bloquear la creación de clientes por el censo rompería todos esos flujos legítimos. El patrón recomendado es otro: **verifica el par nombre + NIF justo antes de emitir facturas VeriFactu a ese cliente** con el endpoint dedicado: ``` POST /v1/clients/census-verification ``` * **Scope:** `clients:read` * **Body de la request:** `tax_id` (NIF/CIF/NIE) y `name` — el par se comprueba **conjuntamente**, exactamente igual que lo comprobará la AEAT en el envío. No necesita corresponder a un cliente existente: la comprobación es **stateless** y no persiste nada en tus clientes. * **Límite de peticiones:** 5 verificaciones por minuto, como el endpoint de la cuenta. ```bash curl -X POST https://api.factuarea.com/v1/clients/census-verification \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"tax_id": "B12345674", "name": "CONSTRUCCIONES PEREZ SL"}' ``` Respuesta (`200`): ```json { "data": { "object": "census_verification", "status": "identified", "verified_name": "CONSTRUCCIONES PEREZ SL", "checked_at": "2026-06-11T09:30:00Z" } } ``` Los valores de `status` son los mismos seis que en la verificación de la cuenta (tabla más abajo). Cómo actuar con ellos para un cliente: * `identified` — emite con normalidad. * `not_identified` — un envío VeriFactu a este destinatario tiene el **rechazo 1239 garantizado**. Pide al cliente su razón social exacta y su NIF antes de emitir. * `not_identified_similar` — (personas físicas) usa el nombre exacto tal como está registrado en la AEAT. * `unavailable` — la AEAT no pudo responder; la comprobación es informativa, así que puedes emitir igualmente y reintentar la verificación más tarde. <Callout type="info"> Si aun así se cuela un envío rechazado, no está todo perdido: la [subsanación](/guides/verifactu-subsanacion) te permite corregir los datos y reenviar el mismo registro. Verificación censal por delante más subsanación como red de seguridad cubren el ciclo completo del 1239. </Callout> ## Estados posibles [#estados-posibles] `status` es siempre uno de estos seis valores: | `status` | Significado | Qué hacer | | ------------------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `identified` | El par razón social + NIF coincide con el censo. | Nada — estás listo para VeriFactu. | | `not_identified` | El NIF no está identificado en el censo con esa razón social. | Revisa tus datos fiscales: razón social exacta y NIF. Los envíos corren riesgo de rechazo. | | `not_identified_similar` | (Solo personas físicas.) El NIF existe pero el nombre solo coincide parcialmente. | Usa el nombre exacto tal y como está registrado en la AEAT. | | `identified_inactive` | El NIF está identificado pero figura de baja en el censo. | Comprueba tu situación censal con la AEAT. | | `identified_revoked` | El NIF está identificado pero ha sido revocado. | Comprueba tu situación censal con la AEAT. | | `unavailable` | No se pudo contactar con el servicio de la AEAT o devolvió una respuesta no reconocida. | Reintenta más tarde. **No** es un error. | Ramifica según `status`, los valores son un contrato congelado. Ten en cuenta que **los estados negativos no son errores HTTP**: toda verificación completada devuelve `200`. ## Fail-open por diseño [#fail-open-por-diseño] La verificación nunca rompe tu flujo porque la AEAT esté caída: * Timeout de la AEAT, SOAP fault o respuesta no reconocida → `200` con `status: unavailable`. Nunca un `5xx` por esta causa. * Los resultados se cachean en el servidor durante un periodo corto, de modo que los reintentos inmediatos del mismo par no vuelven a llamar a la AEAT (`unavailable` se cachea solo unos segundos para que puedas reintentar pronto). ## Errores [#errores] El único error de negocio es una empresa sin datos fiscales: ```json { "error": { "type": "invalid_request_error", "code": "census_requires_tax_id", "message": "Configura primero los datos fiscales de tu empresa para verificar el censo.", "param": "tax_id" } } ``` | HTTP | `code` | Cuándo | | ---- | ------------------------------------- | -------------------------------------------- | | 401 | `missing_api_key` / `invalid_api_key` | API key ausente o inválida. | | 403 | `insufficient_scope` | La clave no tiene el scope `account:read`. | | 422 | `census_requires_tax_id` | La cuenta aún no tiene `tax_id` configurado. | | 429 | `rate_limit_exceeded` | Más de **5 verificaciones por minuto**. | Consulta la [guía del envoltorio de error](/guides/errors) para el contrato completo de errores. ## Límite de peticiones [#límite-de-peticiones] El endpoint está limitado a **5 verificaciones por minuto** por cuenta, independientemente del tier global de límite de peticiones de tu clave. Por encima recibes un `429` — espera a que la ventana se reinicie y reintenta. ## Modo de prueba: NIFs mágicos [#modo-de-prueba-nifs-mágicos] Con una clave `fact_test_` ([modo de prueba](/guides/test-mode)) la verificación **nunca llega a la AEAT**. El sandbox devuelve estados deterministas según el `tax_id` de la empresa sandbox, para que puedas ejercitar todas las ramas de tu integración: | `tax_id` del sandbox | `status` devuelto | | -------------------- | -------------------------- | | `00000000T` | `identified` | | `11111111H` | `not_identified` | | `22222222J` | `not_identified_similar` | | `33333333P` | `identified_inactive` | | `44444444A` | `identified_revoked` | | `55555555K` | `unavailable` | | cualquier otro NIF | `identified` (por defecto) | Todos los NIFs mágicos llevan una letra de control válida, así que pasan la validación estándar de NIF. Configura el `tax_id` de la empresa sandbox con el valor mágico que quieras probar y llama al endpoint con tu clave `fact_test_`: ```bash curl -X POST https://api.factuarea.com/v1/account/census-verification \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` ```json { "data": { "object": "census_verification", "status": "identified_revoked", "verified_name": "SANDBOX COMPANY SL", "checked_at": "2026-06-10T22:15:04+00:00" } } ``` <Callout type="warn"> En producción se consulta el censo real de la AEAT con el certificado de plataforma de Factuarea. Los estados reflejan la respuesta de la AEAT al pie de la letra — Factuarea nunca inventa un estado. </Callout> --- # API keys de empresas hijas (/es/guides/child-api-keys) Cada [empresa gestionada](/guides/companies) tiene su propio juego de API keys. Una key hija autentica peticiones **en nombre de esa única empresa** — nunca alcanza a las empresas hermanas ni al tenant maestro. Es la alternativa a manejar una hija con el [header `X-Active-Profile`](/guides/acting-on-behalf): una key hija queda ligada a una empresa para siempre, en vez de cambiarse por petición. Cinco endpoints bajo `/v1/companies/{id}/api-keys` cubren su ciclo de vida. | Operación | Endpoint | Scope | | ---------------------- | ------------------------------------------------------ | ---------------- | | Listar keys hijas | `GET /v1/companies/{id}/api-keys` | `api_keys:read` | | Crear una key hija | `POST /v1/companies/{id}/api-keys` | `api_keys:write` | | Recuperar una key hija | `GET /v1/companies/{id}/api-keys/{key}` | `api_keys:read` | | Rotar el secret | `POST /v1/companies/{id}/api-keys/{key}/rotate-secret` | `api_keys:write` | | Revocar una key hija | `DELETE /v1/companies/{id}/api-keys/{key}` | `api_keys:write` | Esto replica el autoservicio de [API keys](/guides/api-keys) a nivel de cuenta, pero acotado a una empresa hija en vez de a tu propia cuenta. Tanto `{id}` (la empresa) como `{key}` (la API key) son valores UUID v7 opacos. ## Crea una key hija [#create] `POST /v1/companies/{id}/api-keys` emite una key para la empresa y devuelve su `secret` en texto plano **exactamente una vez**. Requiere el scope `api_keys:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Producción Talleres García", "scopes": ["invoices:read", "invoices:write"] }' ``` Respuesta (`201`): ```json { "data": { "object": "api_key", "id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162", "name": "Producción Talleres García", "prefix": "fact_live_1OSf9KdP", "secret": "fact_live_1OSf9KdPR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "invoices:write"], "tier": "scale", "environment": "live" } } ``` <Callout type="warn"> El `secret` se muestra **solo** en esta respuesta `201` y tras una rotación. Ningún endpoint lo devuelve después. Persístelo en un gestor de secretos en cuanto lo recibas — nunca en un log ni en un repositorio. Si lo pierdes, rota la key. </Callout> ### Los scopes deben ser un subconjunto de la key padre [#subset] Los scopes que pides para una key hija **deben ser un subconjunto de los de la key que hace la llamada**. Pedir un scope que la key llamante no tiene devuelve `422` con errores por campo — **sin recorte silencioso**: la key no se crea con una lista de scopes acortada, falla la petición entera. ```json { "error": { "type": "validation_error", "code": "validation_failed", "message": "No puedes conceder un scope que tu propia key no tiene.", "param": "scopes" } } ``` Así, una key con `invoices:read invoices:write` puede emitir keys hijas con cualquier subconjunto de esos dos scopes, pero nunca con `clients:write`. Aprovisiona primero una key maestra con scopes suficientes y deriva de ella keys hijas más estrechas. El `environment` y el `tier` nunca se toman del cuerpo — se heredan de la key llamante. | Campo | Obligatorio | Notas | | -------------- | ----------- | -------------------------------------------------------------------------------------------------- | | `name` | sí | Etiqueta legible (1–120 caracteres). | | `scopes` | sí | Uno o más [scopes](/guides/authentication#scopes), cada uno subconjunto de los de la key llamante. | | `expires_at` | no | Instante futuro ISO 8601 a partir del cual la key deja de autenticar. | | `ip_allowlist` | no | Lista opcional de IPs / CIDR permitidas (IPv4, IPv6, `/N`). | ## Rota el secret [#rotate] `POST /v1/companies/{id}/api-keys/{key}/rotate-secret` invalida de inmediato el secret actual, genera un nuevo `prefix` + `secret`, y devuelve el nuevo secret en texto plano **exactamente una vez**. Requiere el scope `api_keys:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys/0190f2c0-77aa-7b21-8c33-1d2e3f405162/rotate-secret \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` <Callout type="warn"> La rotación surte efecto **al instante**: cualquier petición que siga usando el secret anterior deja de autenticar en cuanto rotas. Despliega el nuevo secret antes de — o de forma atómica con — la rotación para evitar downtime. Es irreversible. </Callout> ## Revoca una key hija [#revoke] `DELETE /v1/companies/{id}/api-keys/{key}` revoca una key hija de forma permanente. Las peticiones posteriores autenticadas con ella dejan de funcionar. Requiere el scope `api_keys:write`. ```bash curl -X DELETE \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys/0190f2c0-77aa-7b21-8c33-1d2e3f405162 \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` <Callout type="warn"> La revocación es **irreversible**. Una vez revocada, la key no se puede restaurar — emite una nueva si la empresa todavía necesita acceso a la API. </Callout> ## Scopes y aislamiento [#scopes] Las keys hijas se protegen con los scopes de `api_keys`: * `api_keys:read` — listar y recuperar keys hijas. * `api_keys:write` — crear, rotar y revocar keys hijas. Toda operación está acotada a tu tenant maestro. Un `id` de empresa que pertenece a otro maestro devuelve `404` — nunca `403`, ni un endpoint de key hija de una empresa que no gestionas. Es el mismo aislamiento entre maestros que rige [las empresas en sí](/guides/companies#scopes): un maestro solo ve y actúa sobre sus propias empresas y sus keys. --- # Empresas gestionadas (/es/guides/companies) Una **empresa gestionada** es una subcuenta hija que creas y operas bajo tu propio tenant maestro. Es el modelo de gestoría: una asesoría (la maestra) mantiene un único juego de credenciales y, mediante ellas, da de alta y gestiona muchas empresas clientes, cada una aislada de las demás. Aprovisionas cada empresa hija y luego la manejas de dos formas: emites una [API key hija](/guides/child-api-keys) acotada a ella, o mantienes tu master key y cambias de empresa objetivo por petición con el [header `X-Active-Profile`](/guides/acting-on-behalf). Esta página cubre las empresas en sí — crearlas, aprovisionarlas, su ciclo de vida activa/desactivada, el cobro de asientos y el archivado. Once endpoints bajo `/v1/companies` gestionan las empresas. | Operación | Endpoint | Scope | | ----------------------------------- | ----------------------------------------- | ------------------ | | Listar empresas | `GET /v1/companies` | `companies:read` | | Crear una empresa | `POST /v1/companies` | `companies:write` | | Recuperar una empresa | `GET /v1/companies/{id}` | `companies:read` | | Actualizar una empresa | `PATCH /v1/companies/{id}` | `companies:write` | | Archivar una empresa | `DELETE /v1/companies/{id}` | `companies:delete` | | Consultar el estado de creación | `GET /v1/companies/{id}/creation-status` | `companies:read` | | Verificar (reconciliar) la creación | `POST /v1/companies/{id}/verify-creation` | `companies:write` | | Desactivar una empresa | `POST /v1/companies/{id}/deactivate` | `companies:write` | | Reactivar una empresa | `POST /v1/companies/{id}/activate` | `companies:write` | | Activar empresas en bloque | `POST /v1/companies/activate` | `companies:write` | | Previsualizar el cobro del asiento | `GET /v1/companies/seat-charge-preview` | `companies:read` | `{id}` es el `id` de la empresa — un UUID v7 opaco, **no** su `tax_id`. Mira los esquemas completos en la [Referencia de la API](/api-reference/companies/public-api.v1.companies.list). ## Crea una empresa gestionada [#create] `POST /v1/companies` da de alta una nueva empresa hija bajo tu tenant maestro. `name` y `tax_id` son los únicos campos **obligatorios**; el resto del perfil (razón social, dirección fiscal, datos de contacto) es opcional y se puede enviar en la misma petición. El `tax_id` (NIF / CIF / NIE) debe ser **único entre las empresas que ya gestionas**; un duplicado devuelve `409`. Requiere el scope `companies:write`. ```bash 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" }' ``` Respuesta (`201`): ```json { "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 } } ``` ### Cuerpo de la petición [#cuerpo-de-la-petición] | Campo | Obligatorio | Notas | | --------------- | ----------- | ------------------------------------------------------------------------------------- | | `name` | sí | Nombre comercial (1–255 caracteres). | | `tax_id` | sí | Identificador fiscal español (NIF / CIF / NIE). **Inmutable** tras el alta. | | `business_name` | no | Razón social, hasta 100 caracteres. | | `address` | no | Dirección fiscal. | | `city` | no | Ciudad de la sede fiscal. | | `postal_code` | no | Código postal — de él se deriva la zona AEAT (`country_aeat_zone` en las respuestas). | | `province` | no | Provincia. | | `country` | no | País. | | `email` | no | Email de contacto. | | `phone` | no | Teléfono de contacto. | El campo `status` de la respuesta es el [ciclo de vida activa/desactivada](#lifecycle) de la empresa, un eje distinto del [`provisioning_status`](#provisioning) que sigue el aprovisionamiento asíncrono. No hay campo de entrada `country_aeat_zone` — envías un `country` de texto libre, y la zona AEAT se deriva del `postal_code`. <Callout type="info"> En el alta solo se aplica validación a nivel de formulario. La comprobación censal con la AEAT es un paso de aprovisionamiento aparte — dar de alta una empresa aquí no la verifica contra el censo en la misma petición. </Callout> ### Identificador fiscal duplicado [#identificador-fiscal-duplicado] Reutilizar un `tax_id` que ya gestionas devuelve `409` con `resource_already_exists`, y `existing_resource_id` apunta a la empresa que ya lo tiene: ```json { "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" } } ``` El `tax_id` es único **por tenant maestro**, no a nivel global: dos gestorías distintas pueden gestionar cada una una empresa con el mismo identificador fiscal. ## Ciclo de vida del aprovisionamiento [#provisioning] Dar de alta una empresa de facturación real **no es instantáneo**. `POST /v1/companies` responde al momento, pero detrás la empresa hija se **aprovisiona** de forma asíncrona: se crean una serie de documentos por defecto y la config fiscal mínima y — en `live` — se cobra a la gestoría el nuevo asiento. Hasta que eso termina, la hija aún no es operativa. Dos endpoints exponen el ciclo: uno para **consultarlo** y otro para **reconciliarlo**. | Operación | Endpoint | Scope | | ----------------------------------- | ----------------------------------------- | ----------------- | | Consultar el estado de creación | `GET /v1/companies/{id}/creation-status` | `companies:read` | | Verificar (reconciliar) la creación | `POST /v1/companies/{id}/verify-creation` | `companies:write` | ### Estados de aprovisionamiento [#estados-de-aprovisionamiento] `provisioning_status` recorre un ciclo de vida pequeño y de un solo sentido: | Estado | Significado | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | La hija se dio de alta; el aprovisionamiento aún no ha empezado. | | `awaiting_payment` | La gestoría no tiene un método de pago registrado, así que el asiento todavía no se puede cobrar. `payment_setup_url` apunta a donde el tenant maestro añade uno. | | `provisioning` | El asiento se cobró (o la hija está en modo de prueba) y el tenant se está configurando. | | `active` | El aprovisionamiento terminó. La hija es plenamente operativa. | | `failed` | El aprovisionamiento no pudo completarse — `failed_reason` indica el motivo. Vuelve a crear la empresa para reintentarlo. | <Callout type="info"> Con una clave de **prueba** (`fact_test_`) no hay llamada a Stripe: la hija pasa directamente a `active`, de forma determinista. El camino de `awaiting_payment` y el cobro per-seat solo aplican a las claves `live`. </Callout> ### Cobro per-seat [#per-seat] En `live`, cada empresa hija **activa** es un **asiento** que se cobra dentro de la suscripción ya existente del tenant maestro — una sola factura recurrente que se lee como "plan + N clientes". Añadir una hija añade un asiento y **cobra el prorrateo de inmediato** por lo que queda del periodo de facturación; archivar una hija quita el asiento y **abona** el tiempo no usado en la siguiente factura. La hija no llega a `active` hasta que ese cobro inmediato tiene éxito; si falla, la hija acaba en `failed`. Previsualiza el importe de antemano con [el preview del asiento](#seat-charge-preview). <Callout type="info"> Como el asiento vive en la propia suscripción del tenant maestro, la hija hereda el plan y los add-ons del maestro, y un impago de la suscripción del maestro suspende la cuenta entera de la gestoría — sus hijas incluidas. No hay una factura separada por hija. </Callout> ### Consulta el estado de creación [#creation-status] `GET /v1/companies/{id}/creation-status` devuelve el `provisioning_status` actual y las marcas de tiempo. Consúltalo tras crear una empresa hasta que llegue a `active` (o `failed`). Requiere el scope `companies:read`. ```bash curl https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/creation-status \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Respuesta (`200`): ```json { "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` está presente **solo** mientras `awaiting_payment`, y `failed_reason` **solo** cuando `failed`; ambos son `null` en los demás casos. ### Verifica la creación [#verify-creation] `POST /v1/companies/{id}/verify-creation` reconcilia una hija contra la suscripción del tenant maestro y la hace avanzar cuando puede. **No lleva cuerpo de petición** y es **idempotente**. Requiere el scope `companies:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/verify-creation \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Llámalo una vez el tenant maestro haya añadido un método de pago, o cuando quieras sacar a una hija de `awaiting_payment`: * Una hija que ya está `active` es una **operación sin efecto** — la llamada se puede repetir sin riesgo. * Mientras `awaiting_payment`, si el maestro ya tiene un método de pago, se cobra el asiento prorrateado y la hija pasa a `active`. * Si el maestro aún no tiene método de pago, la llamada no tiene efecto ni **error** — la hija permanece en `awaiting_payment`. Devuelve el mismo recurso de creation-status que el endpoint de consulta, así que puedes leer el `provisioning_status` resultante directamente de la respuesta. ## El ciclo de vida activa/desactivada [#lifecycle] Aparte del aprovisionamiento, cada hija lleva un `status` — su **ciclo de vida del vínculo** dentro de la gestoría. Es el campo `status` del recurso de empresa, y recorre tres estados: | Estado | Significado | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `active` | La hija está vinculada y operativa. | | `inactive` | La hija está desactivada — inaccesible hasta que la reactives (pagando de nuevo su asiento), pero con sus datos intactos y de forma reversible. | | `archived` | La hija ha sido desvinculada. Es un estado terminal. | Las transiciones son `active ↔ inactive` (desactivar / reactivar) y `active → archived` o `inactive → archived` ([archivar](#archive)). `archived` es terminal. Desactivar libera el asiento; reactivar lo cobra de nuevo. Esto permite a una gestoría aparcar un cliente entre encargos sin perder su historial, y recuperarlo más tarde. ### Desactiva una empresa [#deactivate] `POST /v1/companies/{id}/deactivate` pasa una hija `active` a `inactive`. La empresa queda inaccesible pero conserva todos sus datos, de forma reversible. **No cobra**: el abono prorrateado del asiento liberado se aplica best-effort en la siguiente factura. Requiere el scope `companies:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/deactivate \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Devuelve el recurso de empresa actualizado con `status: "inactive"`. ### Reactiva una empresa [#activate] `POST /v1/companies/{id}/activate` devuelve una hija `inactive` a `active`. La reactivación está gateada por un **cobro atómico del asiento**: primero se cobra el prorrateo, y solo si el cobro tiene éxito la hija pasa a `active`. Si el maestro no tiene método de pago, o el cobro falla, la empresa sigue `inactive`. Requiere el scope `companies:write`. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/activate \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Devuelve el recurso de empresa actualizado con `status: "active"`. ### Activa empresas en bloque [#activate-batch] `POST /v1/companies/activate` reactiva varias hijas en una sola llamada, con un **único cobro conjunto** — una factura para todo el lote en vez de una por empresa. El cuerpo lleva `company_ids`, una lista de valores `id` de empresa hija (1–1000). Requiere el scope `companies:write`. ```bash 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" ] }' ``` El cobro es atómico **a nivel de lote** — o todas o ninguna. La propiedad (`404` para una empresa fuera de tu árbol) y la precondición `inactive` (`422`) se validan para cada empresa **antes** de que corra ningún cobro. La respuesta es la lista de empresas reactivadas (`{ "data": [ … ] }`). ## Previsualiza el cobro del asiento [#seat-charge-preview] `GET /v1/companies/seat-charge-preview` devuelve lo que **costaría** añadir o reactivar empresas hijas, sin cobrar nada. Úsalo para mostrar el prorrateo antes de un `POST /v1/companies` o de una activación, y para detectar de antemano el caso "sin método de pago". Requiere el scope `companies:read`. Tiene dos modos: * `count` (≥1, default 1) — previsualiza el prorrateo conjunto de activar ese número de hijas en un lote. * `company_ids` — una lista de valores `id` de hijas concretas, para un preview consciente de la cobertura: el importe es `0` con `already_covered: true` cuando todas siguen cubiertas este periodo, y en caso contrario prorratea solo las no cubiertas. Cuando se envía, manda sobre `count`. ```bash curl "https://api.factuarea.com/v1/companies/seat-charge-preview?count=1" \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Respuesta (`200`): ```json { "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` es la **base imponible** del prorrateo en unidades mínimas de la moneda (céntimos), `tax_amount` el IVA, y `total` (`amount + tax_amount`) lo que se cobra realmente. `tax_rate` es el porcentaje de IVA derivado (p. ej. `21`) o `null` si Stripe Tax no lo calculó. Los campos `recurring_*` proyectan la cuota mensual conjunta tras la activación: número total de asientos, base sin IVA y total con IVA (`recurring_total_cents` es `null` cuando el IVA no es calculable). Cuatro flags mutuamente excluyentes explican un importe `0`, por orden de prioridad: | Flag | El `amount` es `0` porque… | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `already_covered` | Las empresas que activarías ya están incluidas en la suscripción de este periodo — reactivarlas es gratis. | | `requires_active_plan` | La gestoría no tiene plan vigente y debe contratar uno antes de gestionar empresas. | | `included_in_trial` | La gestoría está en su periodo de prueba — la empresa se crea gratis (los asientos empiezan a cobrarse cuando el trial se convierte en plan de pago). | | `requires_payment_method` | La gestoría tiene un plan de pago pero ningún método de pago registrado, y debe añadir uno (Billing Portal) primero. | `is_first_seat` es `true` cuando la activación crea la **primera** suscripción de asientos del maestro: el cargo es un mes completo y hoy ancla el día de cobro mensual del ciclo conjunto. ## Lista y recupera empresas [#list] `GET /v1/companies` devuelve tus empresas gestionadas con [paginación por cursor](/guides/pagination); `GET /v1/companies/{id}` devuelve una. Ambas están acotadas a tu tenant maestro. ```bash curl https://api.factuarea.com/v1/companies?limit=25 \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Una empresa gestionada por un tenant maestro **distinto** devuelve `404`, nunca `403` — la API no revela jamás que existe una empresa que no puedes gestionar. ## Actualiza una empresa [#update] `PATCH /v1/companies/{id}` es una **actualización parcial**. El único campo editable es `name` — el perfil (razón social, dirección fiscal, contacto, zona AEAT) no es editable aquí, y el `tax_id` es **inmutable** y se rechaza si lo incluyes en el cuerpo. Una empresa debe estar `active` para editarse. Requiere el scope `companies:write`. ```bash 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" }' ``` ## Archiva una empresa [#archive] `DELETE /v1/companies/{id}` **archiva** la empresa en lugar de borrarla: su `status` pasa a `archived` y deja de aceptar operaciones. Una empresa `active` o `inactive` se puede archivar; `archived` es terminal. Requiere el scope `companies:delete`. ```bash curl -X DELETE \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` <Callout type="warn"> El archivado puede quedar **bloqueado**: si la empresa todavía tiene estado que lo impide (por ejemplo documentos pendientes), la petición devuelve `422` y la empresa conserva su estado actual. Resuelve antes la condición que lo bloquea y luego archívala. </Callout> ## Scopes y aislamiento [#scopes] Las empresas se protegen con sus propios scopes: * `companies:read` — listar y recuperar empresas gestionadas, consultar el estado de creación y previsualizar el cobro del asiento. * `companies:write` — crear, actualizar, activar y desactivar empresas gestionadas, y verificar su creación. * `companies:delete` — archivar empresas gestionadas. Toda operación está acotada a tu tenant maestro. Un `id` de empresa que pertenece a otro maestro devuelve `404` — nunca `403`. Este aislamiento entre maestros es la garantía central del modelo de gestoría: un maestro solo ve y actúa sobre sus propias empresas. La misma garantía rige [actuar en nombre de una hija](/guides/acting-on-behalf) y sus [API keys](/guides/child-api-keys). --- # Facturas rectificativas (/es/guides/corrective-invoices) Una factura rectificativa es un documento fiscal por derecho propio: recibe su propio número, su propia alta ante la AEAT y su propio efecto en la declaración de IVA. Emitirla implica cuatro decisiones independientes, y las integraciones tienden a mezclarlas: 1. **Qué factura** puede rectificarse. 2. **Qué código de rectificación** lleva — el motivo legal. 3. **Sustitución o diferencias** — si la rectificativa declara los importes correctos o solo el delta. 4. **Con qué líneas** acaba la rectificativa. Equivócate a la vez en la tercera y en la cuarta y presentarás una declaración de IVA con el signo invertido. ## Cuándo aplica [#when] La factura original debe estar `sent` o `paid`. Ninguna otra sirve ([`BR-INV-001`](#traceability), RD 1619/2012 art. 15): | Estado del original | ¿Rectificable? | | ----------------------- | ------------------------------------------------------------- | | `sent`, `paid` | Sí. | | `draft`, `cancelled` | No — edítalo o elimínalo, todavía no es un documento fiscal. | | `overdue` | No. Registra antes el cobro o anúlala. | | `annulled` | No — ya se retiró. | | Ya es una rectificativa | No. Emite una rectificativa nueva **de la factura original**. | Para una factura pagada esto no es una opción entre varias: es la única. Una factura pagada no se puede anular, porque su IVA repercutido ya está comprometido con un periodo ([`BR-INV-023`](#traceability)). Ver [Anular o rectificar](/guides/annul-vs-correct). ## La matriz de códigos de rectificación es legal, no cosmética [#r-codes] El código de rectificación declara *por qué* se rectifica el original, y la AEAT restringe qué códigos son legales para cada tipo de original. | Tipo de la factura original | Códigos legales | | ------------------------------- | ------------------ | | Simplificada `F2` | **solo `R5`** | | Completa `F1`, sustitutiva `F3` | **solo `R1`–`R4`** | Fuerza un código fuera de su fila y la API responde `422` con los valores legales en `allowed_values` ([`BR-INV-035`](#traceability)). Hay dos formas de llegar al código. Por defecto se **deriva** del slug `correction_reason` que envías ([`BR-INV-018`](#traceability)): | `correction_reason` | Código | Base legal | | -------------------------------------------------------------------- | ------ | ------------------------------------------------------- | | `error_fundado` | `R1` | Art. 80.Uno, Dos y Seis LIVA — error fundado de derecho | | `concurso` | `R2` | Art. 80.Tres LIVA — concurso de acreedores | | `incobrable` | `R3` | Art. 80.Cuatro LIVA — crédito incobrable | | `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras` | `R4` | RD 1619/2012 art. 15 — resto de causas | `R5` no se deriva nunca de un motivo. Viene del *tipo* del original: una rectificativa de una `F2` nace siempre `R5`, sea cual sea el motivo que pases ([`BR-INV-019`](#traceability)). Como alternativa, fijas `correction_code` de forma explícita. Sobre un original completo, un `R1`–`R4` explícito **gana** a la derivación por slug y pasa a ser el código que viaja en la cadena VeriFactu. Úsalo cuando tu propio sistema ya conozca la causa legal y no quieras que se infiera de un slug. `R2` y `R3` exigen documentación acreditativa por ley. Pasa `justification` (de 10 a 1000 caracteres); se antepone a las notas de la rectificativa como trazabilidad documental. ## Sustitución o diferencias [#nature] Es la decisión de mayor radio de impacto, y en el contrato v1 no la fijas directamente — fijas `correction_type` y la naturaleza se deriva: | `correction_type` | Naturaleza | La rectificativa contiene | Signo | | ----------------- | --------------------- | ------------------------------------------------------------------------ | ------------------------ | | `full` | `S` — sustitución | Los **importes correctos, completos**. Reemplaza al original por entero. | Siempre positivo o cero. | | `partial` | `I` — por diferencias | Solo la **diferencia** entre lo facturado y lo correcto. | Puede ser negativo. | La regla que impone el motor fiscal: una base imponible puede ser negativa **solo** en una rectificativa por diferencias. En una sustitución —y en cualquier factura ordinaria— una base negativa es un dato incoherente y se rechaza con `422` ([`BR-VFC-033`](#traceability), [`BR-INV-017`](#traceability)). Este es el mecanismo para una corrección a la baja. Un abono es una rectificativa por diferencias con base y cuota de IVA negativas, y la AEAT la acepta precisamente porque es la forma fiscalmente correcta de expresar un crédito. Intentar expresar ese mismo abono como una sustitución con importes negativos se rechaza. <Callout type="warn"> Una rectificativa por diferencias sobre una factura al 0 % de IVA tiene **base negativa y cuota cero** — no cuota negativa. El motor fiscal hereda el tipo de las líneas del original y nunca se lo inventa; fabricar aquí un 21 % es la manera clásica de cosechar un rechazo de la AEAT. </Callout> ## Qué envía la API [#api] [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective), scope `invoices:write`. Responde `201` con la **nueva** factura y una cabecera `Location` que apunta a ella. | Campo | Obligatorio | Notas | | ------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `correction_reason` | Sí | Uno de los ocho slugs de arriba. | | `correction_type` | Sí | `full` o `partial`. | | `correction_code` | No | `R1`–`R5`. Se valida contra la matriz legal. | | `justification` | No | De 10 a 1000 caracteres. En la práctica, obligatoria para `R2` y `R3`. | | `notes` | No | Texto libre, hasta 1000 caracteres. | | `lines` | Obligatorio cuando `correction_type` es `partial` | `description`, `quantity`, `unit_price` y, opcionalmente, `tax_rate`, `discount_percent`, `indirect_tax_regime`, `product_id`. | La API Reference publica un ejemplo listo para enviar por cada código — `r1_error_fundado`, `r2_concurso`, `r3_incobrable`, `r4_otras` y `r5_simplificada` — en el desplegable de ejemplos del cuerpo de petición de esa operación. Se publican además como entradas reutilizables `components.examples.corrective_*` del documento OpenAPI, de modo que los clientes generados puedan resolverlas por `$ref`. ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/corrective \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "correction_reason": "otras", "correction_type": "partial", "correction_code": "R3", "justification": "Crédito declarado incobrable por resolución judicial firme.", "lines": [ { "description": "Ajuste por impago", "quantity": -1, "unit_price": 100, "tax_rate": 21 } ] }' ``` La respuesta es un objeto factura ordinario cuyo `is_corrective` vale `true` y cuyo bloque `corrective` lleva `original_id`, `original_number`, `original_date`, `correction_reason`, `correction_type`, `correction_nature`, `base_rectificada`, `cuota_rectificada` y `correction_aeat_type` — este último es el código de rectificación que viajó de verdad a la AEAT. Para listar todas las rectificativas emitidas contra una factura, usa [`GET /v1/invoices/{id}/correctives`](/api-reference/invoices/public-api.v1.invoices.correctives). ### Cómo se construyen las líneas [#lines] Las tres combinaciones producen documentos genuinamente distintos ([`BR-INV-036`](#traceability)): **`full` sin `lines`** — una anulación completa. Se genera una línea por cada línea del original con la cantidad **negada**, conservando el producto, el precio, el tipo impositivo, la retención, el recargo, el descuento y el régimen indirecto del original. **`full` con `lines`** — una sustitución. Las líneas que envías **son** las líneas finales; no hay comparación de diferencias. Cada campo que omitas se hereda **por índice** de la línea original equivalente, fiscalidad incluida. La herencia nunca cae a un tipo por defecto, así que una operación exenta sigue exenta en vez de adquirir un 21 % fantasma. Si envías más líneas de las que tenía el original, las sobrantes no tienen contraparte: no llevan producto y su fiscalidad queda a cero. **`partial`** — líneas de ajuste. No hay línea original con la que casar por índice, así que los valores por defecto son cero y `product_id` viaja solo si lo declaras explícitamente. Una línea sin `product_id` no mueve inventario. <Callout type="info"> La herencia por índice da por supuesto que las líneas rectificadas llegan **en el mismo orden** que las originales. Reordenar o suprimir líneas cruza los valores heredados. Cuando quien te llama reordene, declara los campos de forma explícita en cada línea en lugar de apoyarte en la herencia. </Callout> Las líneas de suplido también se heredan del original, y por eso el payload de la rectificativa acepta `line_type` y `source_invoice_reference` en una línea. Ver [Suplidos](/guides/disbursements). ## Qué sale en el PDF [#pdf] La rectificativa se imprime como documento aparte con su propio número, derivado del original: `SERIE-AAAA-NNN-REC{n}`, donde `{n}` cuenta las rectificativas ya emitidas contra ese original ([`BR-INV-021`](#traceability)). Sus bloques de destinatario y emisor se congelan **en su propio momento de emisión**, no se copian del original. Es deliberado: un motivo habitual para rectificar es precisamente que los datos del destinatario estaban mal, así que la rectificativa debe imprimir los corregidos ([`BR-INV-024`](#traceability)). Como cualquier factura emitida por una empresa adherida a VeriFactu, lleva el bloque QR legal. ## Qué llega a la AEAT [#aeat] **Como registro VeriFactu**, la rectificativa es un alta ordinaria cuyo `invoice_type` es el código de rectificación. Su desglose fiscal lleva el signo descrito en [Sustitución o diferencias](#nature): base y cuota negativas para una corrección a la baja por diferencias, siempre no negativas para una sustitución. La sustitución declara además la base y la cuota rectificadas del original; una corrección por diferencias no lo hace, en línea con el esquema de la AEAT ([`BR-VFC-033`](#traceability)). **En la declaración trimestral de IVA**, la rectificación de una operación en régimen general aterriza en las casillas `[14]` y `[15]` **con su signo**: una corrección a la baja resta, una al alza suma. El snapshot fiscal conserva el código real (`R1`–`R4`) en lugar de colapsar toda rectificativa a `R5` ([`BR-TXR-019`](#traceability)). Ese encaminamiento aplica solo al régimen general. Una rectificativa cuyo régimen de operación de cabecera sea otro sigue las casillas propias de ese régimen — la inversión del sujeto pasivo y las operaciones exentas o de exportación se declaran en otro sitio y por tanto **no** llegan a `[14]`/`[15]`. Ver [Claves de régimen](/guides/regime-keys) para saber cómo se determina el régimen de cabecera. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-INV-001` — una rectificativa debe referenciar un original emitido; los estados admisibles. * `BR-INV-017` — la naturaleza de la rectificación es exactamente `S` o `I`. * `BR-INV-018` — el mapeo de slug de motivo a `R1`–`R4`. * `BR-INV-019` — una rectificativa de una `F2` nace `R5`. * `BR-INV-021` — la numeración `-REC{n}` de las rectificativas. * `BR-INV-024` — el snapshot inmutable del destinatario, congelado en el momento de emisión de la propia rectificativa. * `BR-INV-035` — `correction_code` explícito, la matriz legal de la AEAT y `justification`. * `BR-INV-036` — cómo se generan las líneas de la rectificativa y qué se hereda por índice. * `BR-VFC-033` — base y cuota negativas admitidas solo en rectificaciones por diferencias. * `BR-TXR-019` — el signo en las casillas `[14]`/`[15]` y la conservación del código de rectificación real. --- # Suplidos (/es/guides/disbursements) Un *suplido* es una cantidad pagada **en nombre y por cuenta del cliente**, bajo su mandato expreso (art. 78.Tres.3 de la Ley del IVA). No forma parte de lo que cobras por tu servicio: lo anticipas, lo repercutes a coste y nunca llega a ser tu base imponible. Facturado como línea ordinaria, ese mismo importe infla tu base imponible, tu IVA repercutido, el total que declaras a la AEAT y la base que informas de ese cliente en la declaración anual de operaciones con terceros (**Modelo 347**). Facturado como suplido, aparece en el documento, el cliente lo paga y queda fuera de las cuatro cosas. ## Cuándo aplica [#when] Solo en **facturas emitidas**. Los presupuestos, las proformas, los albaranes, las facturas de compra y las plantillas de recurrentes no modelan los suplidos en absoluto — sus tablas de líneas no tienen esa columna ([`BR-INV-037`](#traceability)). Una plantilla de recurrente, en particular, no podría llevar la referencia de origen obligatoria, así que la línea degradaría en silencio a una operación ordinaria y cada factura generada la declararía como ingreso propio. Dos restricciones más: * **Una factura simplificada no puede llevar ninguno.** El contenido obligatorio de una factura simplificada no identifica al destinatario, así que no puede acreditar por cuenta de quién se pagó el importe, y la Administración tributaria lo trataría como base imponible tuya. Su rectificativa se rechaza por el mismo motivo. Emite una factura completa o quita la línea ([`BR-INV-040`](#traceability)). * **Una factura no puede estar hecha solo de suplidos.** Se exige al menos una línea ordinaria ([`BR-INV-046`](#traceability)). <Callout type="warn"> La API solo puede imponer una de las tres condiciones legales: que puedas justificar el importe. El **mandato expreso del cliente** es un requisito documental que Factuarea ni pide ni guarda: sin él el importe no es un suplido, lo etiquete como lo etiquete la factura. Y **el IVA soportado de un suplido no es deducible por ti** — el destinatario real de esa operación es el cliente. Nada en el producto te impide deducirlo, así que esto queda de tu mano. </Callout> ## Qué envía la API [#api] Cuatro campos opcionales de línea en [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create), [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update) y [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective): | Campo | Reglas | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `line_type` | `NORMAL` o `SUPLIDO`. Ausente o `null` equivale a `NORMAL`, así que omitirlo reproduce exactamente el comportamiento anterior. | | `source_invoice_reference` | **Obligatorio** en una línea de suplido. Texto libre, hasta 100 caracteres. | | `source_invoice_ids` | Trazabilidad opcional: facturas de compra **de tu propia empresa**, validadas con alcance de tenant. Una lista vacía colapsa a nulo. | | `line_total` | Suma de control opcional de entrada — ver [La suma de control de línea](#checksum). | ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Honorarios de constitución de sociedad", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }, { "description": "Tasa del Registro Mercantil", "quantity": 1, "unit_price": 150, "line_type": "SUPLIDO", "source_invoice_reference": "RM-2026-0451" } ] }' ``` `POST /v1/invoices` no tiene campo `type`, así que no puede emitir una factura simplificada; el rechazo por factura simplificada solo se alcanza, por tanto, a través del endpoint de rectificativa sobre un original simplificado. Ver [Facturas simplificadas o completas](/guides/simplified-vs-full-invoices#f2-not-in-v1). ### La referencia de origen es obligatoria, y es texto [#reference] Es texto libre y no una clave ajena porque el justificante —una tasa judicial, un arancel registral, un visado— rara vez está registrado como factura de compra en Factuarea. Sin él no puedes acreditar que el gasto pertenece al cliente ([`BR-INV-038`](#traceability)). `source_invoice_ids` es la contraparte estructurada opcional, y la regla práctica conviene interiorizarla: > **Si el justificante está a tu nombre, no es un suplido.** > Factúralo como línea ordinaria. El suplido canónico tiene el documento expedido a nombre del **cliente**, así que no es una compra tuya y la lista se queda vacía. Enlaza facturas de compra solo cuando hayas registrado de verdad el pago en tus propios libros como soporte del anticipo — y recuerda que el IVA soportado de esa factura no debe deducirse. ### Una línea de suplido no lleva carga fiscal propia [#no-tax] Ocho campos se rechazan en una línea `SUPLIDO` con valor distinto de cero o de nulo ([`BR-INV-039`](#traceability)): | Campo | Por qué | | ------------------ | ---------------------------------------------------------------------------------------- | | `tax_rate` | Un suplido no es contraprestación — no le repercutes IVA. | | `retention_rate` | No hay ingreso tuyo sobre el que retener. | | `surcharge_rate` | El recargo de equivalencia grava una entrega tuya; esto no lo es. | | `discount_percent` | Descontar un importe pagado por cuenta ajena lo distorsiona — repercutes lo que pagaste. | | `regime_key` | Una clave de régimen califica una operación tuya. | | `exemption_reason` | Un suplido ni tributa ni está exento: no es operación tuya. | | `product_id` | No es una entrega de bienes tuyos y no debe mover stock. | | `pack_id` | Mismo motivo — un pack se expande en entregas propias. | El error nombra el campo infractor, y lo lleva como `offending_field` en el detalle del error. Como una línea de suplido no puede referenciar un producto, el libro de stock la ignora **por construcción**: la fila persistida no tiene producto y ya queda filtrada. ### La suma de control de línea [#checksum] `lines[].line_total` es una suma de control **de entrada y opcional**. Cuando viene, se compara con el total que el motor acaba de calcular, y la petición se rechaza si la desviación supera un céntimo ([`BR-INV-044`](#traceability)). El detalle del error lleva los valores `expected` y `received` para que localices un descuadre de redondeo con tu ERP sin tener que parsear el mensaje. Tres propiedades, todas deliberadas: * **Nunca se persiste, nunca se devuelve.** No existe esa columna y ningún recurso la emite. El importe facturado es siempre el que calcula Factuarea. * **Nunca es obligatoria**, en ningún escenario. Exigirla te obligaría a replicar nuestro motor de cálculo, algo explícitamente fuera de alcance. * **La tolerancia de un céntimo es inclusiva.** Una desviación de exactamente 0,01 € pasa; 0,02 € falla. La comparación se hace en aritmética de precisión arbitraria, no en coma flotante — el error de coma flotante es precisamente lo que este campo existe para diagnosticar. ### Errores [#errors] Todos `422`: | `subcode` | Causa | | ------------------------------------------- | ------------------------------------------------------------------------------- | | `suplido_requires_source_invoice_reference` | La línea de suplido no tiene referencia de origen. | | `suplido_line_cannot_carry_taxes` | Se envió uno de los ocho campos prohibidos. | | `suplido_not_allowed_in_simplified_invoice` | Una factura simplificada o su rectificativa. | | `invoice_requires_at_least_one_line` | Todas las líneas son suplidos, así que la factura no declara ninguna operación. | | `line_total_checksum_mismatch` | El total de línea declarado se desvía más de un céntimo. | El índice del mensaje empieza en cero sobre la colección completa de líneas, de modo que casa con la ruta `lines.{i}` de tu payload. ## Cómo quedan los totales [#totals] La calculadora de totales particiona las líneas por tipo ([`BR-INV-041`](#traceability)): | Campo | Contenido | | ---------------------------------- | -------------------------------------------------------------- | | `subtotal`, `taxes_total`, `total` | Solo las líneas ordinarias. La fórmula queda intacta. | | `total_disbursements` | La suma de las líneas de suplido, y **solo** eso. Persistido. | | `total_to_pay` | `total + total_disbursements`. **Derivado**, nunca almacenado. | Para la factura de arriba: subtotal 1000, IVA 210, total 1210, suplidos 150, importe a pagar 1360. Hay exactamente un punto del código donde se suman esos dos términos, y todos los consumidores —recursos de la API, el PDF, el enlace público del documento— leen el valor derivado en vez de recomponer la suma. Dos columnas llamadas «total» acabarían divergiendo. **Toda cifra por factura que mide deuda usa el importe a pagar, no el total fiscal** ([`BR-INV-045`](#traceability)): `pending_amount` es `total_to_pay − paid_amount`, el libro de cobros acepta un pago que cubra el importe a pagar íntegro sin responder «supera lo pendiente», la transición a `paid` exige el importe a pagar cubierto —pagar solo el total fiscal deja la factura sin cobrar con el suplido pendiente— y los tres enlaces de pago en línea cobran el importe a pagar. Las cifras **agregadas** de cartera son la excepción documentada: miden volumen facturado, no importe debido. Ese límite, y el que afecta a los documentos Facturae y UBL, están recogidos en [Alcance y limitaciones](/guides/scope-and-limitations#gaps). Una factura sin suplidos tiene `total_disbursements: 0` y `total_to_pay == total`, al céntimo, incluida toda factura histórica. ## Qué sale en el PDF [#pdf] El suplido **sí** se imprime —el cliente lo pagó y la factura es la representación legal de eso— pero marcado como lo que es ([`BR-INV-042`](#traceability)): la línea muestra un guion en la columna de IVA, y el bloque de totales gana una fila *Suplidos* y una fila *Total a pagar* debajo del total fiscal. El enlace público del documento muestra lo mismo. La exportación a hoja de cálculo a nivel de línea añade una columna de tipo de línea, porque sin ella un suplido es **indistinguible de una operación al 0 % de IVA** y sumar la columna de total de línea daría el importe cobrado en lugar del ingreso declarable. Dos campos de línea que son solo de presentación ayudan aquí y no tienen efecto fiscal alguno ([`BR-INV-043`](#traceability)): `unit`, una unidad de medida de texto libre impresa junto a la cantidad, y `exemption_reason_text`, texto libre impreso bajo la descripción para la redacción de la exención cuando la causa catalogada no la cubre. ## Qué llega a la AEAT [#aeat] **Nada.** Una línea de suplido no llega nunca al registro de facturación VeriFactu: ni al desglose fiscal, ni al total declarado ([`BR-VFC-036`](#traceability)). La exclusión ocurre en un **único punto**, la pasarela de lectura, aguas arriba del constructor del desglose — así el mismo conjunto filtrado alimenta a todos los consumidores: el array de líneas, el tipo de IVA agregado, la descripción de la operación, la clave de régimen y el generador de XML. Filtrar solo el array de líneas habría dejado abiertos los demás caminos: un suplido en primera posición donaba un tipo del 0 % al agregado de una factura que sí repercute IVA, y describía la operación a la AEAT como «Tasa del Registro…». El total declarado no cambia de fórmula y excluye los suplidos por construcción, porque el total fiscal agrega solo las líneas ordinarias. La AEAT valida ese total contra la suma del desglose; añadir el suplido descuadraría el registro y provocaría su rechazo. El importe a pagar es presentación y **no se transmite nunca**. **En la declaración anual de operaciones con terceras personas**, la base declarada de cada contraparte es ([`BR-TXR-023`](#traceability)): ``` base = total facturado (IVA incluido) + retención de IRPF − suplidos ``` La retención **suma** —la contraparte recibió una factura por el importe bruto— y el suplido **resta**, porque solo lo repercutiste por cuenta de tu cliente. Invertir cualquiera de los dos signos declara mal a la contraparte. Mientras el término de suplidos fue un cero fijado a fuego, la declaración **sobredeclaraba** a todo cliente al que se le hubieran repercutido tasas o aranceles, con riesgo de descuadre contra su propia declaración cruzada. Las facturas de compra no modelan ni retención ni suplidos, así que ambos términos son estructuralmente cero en el lado recibido. Que un tercero se declare o no se decide **en el contacto**, no en la factura. El campo `accumulate_347` del cliente —escribible desde la v1 en [`POST /v1/clients`](/api-reference/clients/public-api.v1.clients.create) y [`PUT /v1/clients/{id}`](/api-reference/clients/public-api.v1.clients.update), con valor por defecto `true`— excluye todas las operaciones de ese cliente cuando vale `false`, y se lee en vivo al calcular la declaración en lugar de congelarse al emitir ([`BR-TXR-037`](#traceability)). La antigua marca por factura sobrevive como **override dormido**, expuesta en solo lectura en el objeto factura de la v1 como `exclude_347`: puede forzar la exclusión de una factura concreta, nunca reincluir a un tercero ya marcado como no acumulable, y la API pública no la fija ([`BR-TXR-024`](#traceability)). Ninguna de las dos marcas reincluye lo que las reglas automáticas ya excluyeron —operaciones intracomunitarias, exportaciones y facturas simplificadas sin NIF—. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-INV-037` — el catálogo cerrado de tipos de línea `NORMAL|SUPLIDO`, su valor por defecto retrocompatible y por qué existe solo en las facturas emitidas. * `BR-INV-038` — la referencia de origen obligatoria, la trazabilidad opcional a facturas de compra y las dos condiciones legales que el software no puede imponer. * `BR-INV-039` — los ocho campos que una línea de suplido no puede llevar. * `BR-INV-040` — sin suplidos en una factura simplificada ni en su rectificativa. * `BR-INV-041` — suplidos fuera de la base, del IVA y del total; el agregado persistido y la fórmula única derivada del importe a pagar. * `BR-INV-042` — qué superficies excluyen el suplido y cuáles lo muestran marcado. * `BR-INV-043` — `unit` y `exemption_reason_text` como campos de sola presentación. * `BR-INV-044` — `line_total` como suma de control de entrada, opcional, nunca persistida, con tolerancia inclusiva de un céntimo. * `BR-INV-045` — el saldo pendiente medido contra el importe a pagar. * `BR-INV-046` — una factura no puede componerse solo de suplidos. * `BR-VFC-036` — los suplidos no llegan nunca al registro de facturación, y la invariante de huella idéntica en las facturas que no los llevan. * `BR-TXR-023` — la base de la declaración de operaciones con terceros: total facturado más retención menos suplidos. * `BR-TXR-037` — la acumulación en esa declaración se decide en el contacto, se lee en vivo, y la marca por factura queda como override dormido. * `BR-TXR-024` — la marca de exclusión por documento, superseded por `BR-TXR-037` y conservada como ese override. --- # Facturación de asientos de empleado (/es/guides/employee-seats) Los empleados se facturan mediante un **add-on por asiento**, no por el límite `users` del plan — un empleado **nunca** computa contra ese límite. El add-on es una **suscripción mensual dedicada** (`employee-seats`), totalmente separada de la suscripción del plan: su `quantity` sigue el número de **empleados activos**, y contratarlo activa el módulo `control_horario`. Todos los endpoints viven bajo `https://api.factuarea.com/v1` y usan `employees:read` (estado, preview) o `employees:write` (contratar, cambiar cantidad, cancelar). ## Cómo se facturan los asientos [#model] Un **asiento pagado cubre todo el periodo** de facturación. El número de asientos sigue tu plantilla activa de forma automática: * **Activar o dar de alta** un empleado cuyo asiento no está cubierto cobra un asiento prorrateado por lo que resta del periodo. * **Dar de baja** un empleado libera el asiento **sin crédito** (el periodo ya está pagado) pero conserva su cobertura, así que **reactivarlo** dentro del mismo periodo es **gratis**. * Cada **renovación del periodo** refresca la cobertura de los empleados activos en ese momento. La cantidad se mantiene sincronizada con el número real de activos mediante eventos del empleado y una reconciliación horaria, así que rara vez necesitas fijarla a mano. <Callout type="info"> Para una cuenta enterprise facturada **por contrato** (sin suscripción Stripe), el add-on se concede gratis: sin cobro, sin método de pago exigido, y el módulo `control_horario` se habilita igualmente. Cancelar retira el módulo de inmediato. </Callout> ## Consultar el estado de facturación [#status] `GET /v1/employee-seats` devuelve el estado del add-on: si la suscripción está activa (`subscribed`), cuántos asientos se facturan (`quantity`), cuántos empleados están activos, y el coste recurrente por asiento con IVA incluido. **Los importes van en céntimos (unidades menores)** y son `null` —nunca un `0` engañoso— cuando el coste no es resoluble (sin suscribir, sin plan activo, enterprise fuera de Stripe, sandbox). ```bash curl https://api.factuarea.com/v1/employee-seats \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ## Previsualizar el cargo [#preview] `GET /v1/employee-seats/preview` devuelve el importe por asiento **prorrateado** por activar o dar de alta, calculado desde la próxima factura de Stripe, **sin cobrar**. Nunca lanza error — degrada a un preview neutro. | Parámetro | Notas | | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `count` | Preview en lote para N asientos (≥1, hasta 1000). | | `employee_ids` | Preview consciente de la cobertura por UUID v7: los empleados aún cubiertos este periodo cuestan `0` (`already_covered: true`). | `amount` es la base imponible en céntimos; `requires_payment_method` es `true` cuando no hay método de pago archivado. ```bash curl -G https://api.factuarea.com/v1/employee-seats/preview \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "count=3" ``` ## Contratar el add-on [#subscribe] `POST /v1/employee-seats/subscribe` contrata (opt-in): crea la suscripción mensual `employee-seats` con `quantity` igual a tus empleados activos y cobra el primer periodo con el método de pago archivado. El cobro es **atómico** — si no cuaja, **no** se contrata nada: * Sin método de pago → `402 employee_seat_payment_method_required`; el envoltorio de error lleva `error.details.payment_setup_url` para completar el alta de la tarjeta. * Un cobro rechazado → `402 employee_seat_charge_failed`. ```bash curl -X POST https://api.factuarea.com/v1/employee-seats/subscribe \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` <Callout type="warn"> Si contratar devuelve `402 employee_seat_payment_method_required`, envía al usuario al `payment_setup_url` del error, deja que añada una tarjeta y reintenta. No se cobra ni se contrata nada hasta que el primer periodo cuaja. </Callout> ## Sincronizar la cantidad y cancelar [#manage] `POST /v1/employee-seats/change-quantity` reconcilia el número de asientos facturados con el número real de empleados activos (un `SET` sin prorrateo ni factura). Es **idempotente** — un no-op cuando la cantidad ya coincide. `POST /v1/employee-seats/cancel` cancela el add-on **a fin de periodo**: el mes en curso ya está pagado, así que `subscribed` sigue `true` hasta que el periodo termina, y la cobertura por empleado se purga entonces. La **suscripción del plan nunca se toca**. ```bash curl -X POST https://api.factuarea.com/v1/employee-seats/cancel \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Consulta los esquemas en la [Referencia de API](/api-reference/employees/public-api.v1.employee-seats.status). ## Flujo típico [#flow] 1. **Previsualiza** el cargo de los asientos que vas a activar. 2. **Contrata** el add-on (primer periodo cobrado de forma atómica). 3. Añade o quita empleados — la **cantidad se autosincroniza**; reconcilia de forma explícita con change-quantity si hace falta. 4. Lee el **estado** para mostrar los asientos facturados y el coste por asiento. 5. **Cancela** a fin de periodo cuando ya no lo necesites. ## Próximos pasos [#next] * [Visión general del control horario](/guides/workforce-overview) — el rol de empleado solo-portal y todo el sistema. * [Empresas gestionadas](/guides/companies) — facturación por asiento de las empresas hijas de gestoría. --- # Gestión de errores (/es/guides/errors) Toda respuesta de error de la API pública usa un envoltorio JSON consistente. El estado HTTP indica la categoría general; el campo `type` desambigua y el campo `code` apunta a la causa específica. ## Envoltorio [#envoltorio] ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "El campo client_id es obligatorio.", "param": "client_id", "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA", "doc_url": "https://docs.factuarea.com/guides/errors#parameter_invalid" } } ``` Campos: * `type` — categoría general del error. Estable y enumerada (lista abajo). * `code` — causa específica. Estable y enumerada. * `subcode` — opcional. Presente cuando el `code` por sí solo es ambiguo: en los conflictos de duplicación `409` señala la clave duplicada exacta (p. ej. `subcode: "tax_id_already_exists"`), y en los errores de pago `402` señala qué gate rechazó la llamada (p. ej. `subcode: "webhooks_addon_required"`). Como el `code`, es estable e invariante entre idiomas y versiones de la API. * `message` — texto para personas **en español**. **No** se garantiza estable entre versiones; útil para logging y visualización. * `param` — opcional, presente en errores de validación. Apunta al **primer** campo problemático. En errores de validación de varios campos el conjunto completo está en `errors[]` (ver abajo). * `errors[]` — opcional, presente en errores de validación `422`. Lista **todos** los campos fallidos (ver [Errores de validación de varios campos](#errores-de-validación-de-varios-campos)). * `details` — opcional. Lleva `existing_resource_id` en los conflictos de duplicación `409` (ver [Conflictos de duplicación](#conflictos-de-duplicación)) y `payment_setup_url` en los errores `402` que necesitan un método de pago configurado (ver [payment\_required\_error](#payment_required_error)). * `doc_url` — opcional. Enlace a esta guía con ancla al `code` específico (`#{code}`). * `request_id` — identificador único de la petición (`req_<ULID>`). Inclúyelo siempre cuando contactes con soporte. También se devuelve en el header de respuesta `X-Request-Id`. El objeto `error` siempre lleva `type`, `code` y `message`; el resto de campos están presentes cuando es relevante. ## Errores de validación de varios campos [#errores-de-validación-de-varios-campos] Un error de validación `422` reporta **todos** los campos fallidos, no solo el primero. Los `param`/`message` planos siguen reflejando el primer campo (por retrocompatibilidad), y `errors[]` lleva un item por campo fallido — así corriges todos en una sola petición en vez de una petición por campo. ```json { "error": { "type": "invalid_request_error", "code": "invalid_param_value", "message": "El campo client_id es obligatorio.", "param": "client_id", "errors": [ { "param": "client_id", "code": "parameter_missing", "message": "El campo client_id es obligatorio." }, { "param": "issue_date", "code": "parameter_invalid_format", "message": "El formato de la fecha no es válido.", "expected_format": "YYYY-MM-DD" }, { "param": "status", "code": "parameter_invalid_enum", "message": "El valor no es válido.", "allowed_values": ["draft", "sent", "paid"] } ], "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA", "doc_url": "https://docs.factuarea.com/guides/errors#invalid_param_value" } } ``` Cada item de `errors[]` lleva: * `param` — el nombre del campo fallido. * `code` — un código estable y machine-readable derivado de la regla de validación fallida (p. ej. `parameter_missing`, `parameter_invalid_format`, `parameter_invalid_enum`, `parameter_invalid_integer`). * `message` — descripción legible del error del campo. * `expected_format` — opcional. Presente solo en errores de formato; el patrón esperado (p. ej. `YYYY-MM-DD`, `uuid`, `email`, `url`). * `allowed_values` — opcional. Presente solo en errores de enum; la lista de valores legales. `errors[]` es puramente aditivo — las integraciones que solo leen `param`, `code` y `message` siguen funcionando sin cambios. ## Conflictos de duplicación [#conflictos-de-duplicación] Un conflicto de duplicación `409` (`code: resource_already_exists` con un `subcode` de `tax_id_already_exists`, `external_id_already_exists` o `sku_already_exists`) devuelve el id del recurso preexistente en `details.existing_resource_id`. Resuélvelo con un solo `GET` en vez de un `find_by_*` adicional. ```json { "error": { "type": "conflict_error", "code": "resource_already_exists", "subcode": "tax_id_already_exists", "message": "Ya existe un cliente con ese NIF.", "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" }, "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM", "doc_url": "https://docs.factuarea.com/guides/errors#resource_already_exists" } } ``` Un `GET /v1/clients/0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40` devuelve el recurso existente (`200`). Lo mismo aplica en `PUT` cuando un `external_id` ya pertenece a otro recurso de la empresa. ## Detalles del problema — Problem Details (RFC 9457) [#detalles-del-problema--problem-details-rfc-9457] Envía `Accept: application/problem+json` para recibir el mismo error como un documento Problem Details de [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) con `Content-Type: application/problem+json`. Con `Accept: application/json`, `Accept: */*` o sin header `Accept` obtienes el envoltorio plano de arriba. ```json { "type": "https://docs.factuarea.com/errors/resource_already_exists", "title": "Resource already exists", "status": 409, "detail": "Ya existe un cliente con ese NIF.", "instance": "/v1/clients", "code": "resource_already_exists", "subcode": "tax_id_already_exists", "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" }, "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM" } ``` * `type` — la página de documentación de ese `code` concreto, por ejemplo `https://docs.factuarea.com/errors/resource_already_exists`. El `code` es el único segmento variable, así que puedes construir y comparar la URI por tu cuenta. Antes era una sola URI compartida por todos los problemas: si comparas contra ella, compara mejor contra `code`, que no se mueve nunca. * `title` — un resumen humano breve del tipo de problema. * `status` — el código de estado HTTP. * `detail` — el mensaje legible. * `instance` — el path del recurso afectado. La variante problem+json **no descarta** ningún dato extendido: `code`, `subcode`, `param`, `errors[]`, `details`, `doc_url` y `request_id` se conservan como miembros de extensión RFC 9457. ## Mensajes localizados [#mensajes-localizados] El `message` (y el `detail` de problem+json) se localiza vía el header `Accept-Language` para los códigos del catálogo estable. Los idiomas soportados son `es`, `en` y `ca`, con fallback a `es` cuando el header está ausente o pide un idioma no soportado. El `code` y el `subcode` son **invariantes** entre idiomas — ramifica siempre por `code`, nunca por `message`. ``` Accept-Language: en → mensaje en inglés Accept-Language: ca-ES → mensaje en catalán (ausente / Accept-Language: de) → mensaje en español (fallback) ``` Los mensajes dinámicos emitidos por excepciones de dominio quedan en español; solo se localizan los mensajes del catálogo estable. ## Tipos de error [#tipos-de-error] | type | HTTP | Descripción | | --------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_request_error` | `400` o `422` | Payload malformado, parámetros faltantes/inválidos o fallo de validación de negocio. | | `authentication_error` | `401` | La API key falta, es inválida, está revocada, ha expirado o la IP no está en la lista de acceso. | | `payment_required_error` | `402` | La operación cobra dinero y el pago no ha podido completarse: no hay método de pago configurado, el cargo se denegó, o hace falta una suscripción o un add-on que no está contratado. | | `authorization_error` | `403` | La key es válida pero el scope no cubre el endpoint. | | `permission_error` | `403` | El plan de la empresa no da acceso a la funcionalidad. | | `not_found_error` | `404` | El recurso solicitado no existe o no pertenece a la empresa de la key. | | `conflict_error` | `409` | Conflicto de creación, lock de idempotencia o recurso duplicado (p. ej. un `tax_id` ya registrado). | | `idempotency_error` | `409` | Reutilización de `Idempotency-Key` con un payload distinto. | | `rate_limit_error` | `429` | Superada la cuota por minuto o mensual, o demasiados fallos de autenticación. | | `api_error` | `500` | Error inesperado del backend. Los reintentos pueden ayudar; reporta a soporte con el `request_id`. | | `service_unavailable_error` | `503` | API pública deshabilitada vía kill-switch, o caída de una dependencia (Stripe, mailer). | <Callout type="info"> **`402` y `403` no son intercambiables.** Un `402` (`payment_required_error`) significa que la operación está a tu alcance y lo único que se interpone es el dinero: configura un método de pago, resuelve el cargo denegado, o contrata el plan o el add-on al que se factura. Un `403` significa que el acceso en sí está denegado —o la key no tiene el scope (`authorization_error`), o el plan de la empresa no incluye la funcionalidad (`permission_error`)— y ningún reintento de pago lo cambia. La pareja `addon_required` (`402`, el add-on no está contratado) y `addon_not_active` (`403`, ninguna key llega a una funcionalidad no contratada) es la que conviene leer dos veces. </Callout> <Callout type="info"> Las **violaciones de reglas de negocio** (transición de estado inválida, una acción no permitida en el estado actual del documento) responden `422` con `type: invalid_request_error` y `code: invalid_status_transition` — **no** `409`. `409 conflict_error` se reserva para creación duplicada, conflictos de idempotencia y locks de concurrencia. </Callout> ## Catálogo de codes [#catálogo-de-codes] El ancla de cada encabezado H3 coincide exactamente con el valor del campo `code` del envoltorio. El `doc_url` que devuelve la API resuelve a la sección específica. La lista de abajo cubre los codes que encontrarás en la práctica; la referencia OpenAPI en vivo documenta los codes exactos por endpoint. <Callout type="info"> Para la referencia **completa** de cada error `code` agrupado por bounded context, con su estado HTTP y type, consulta [Todos los error codes](/guides/errors/all). </Callout> ### invalid\_request\_error [#invalid_request_error] ### parameter\_invalid [#parameter_invalid] Un parámetro de la petición falta o es inválido. `param` apunta al campo problemático (p. ej. `client_id`, `lines[0].quantity`). ### parameter\_invalid\_format [#parameter_invalid_format] El formato de un valor es incorrecto para su semántica (regex, longitud, codificación, un UUID malformado, una fecha fuera de formato). ### parameter\_invalid\_range [#parameter_invalid_range] Un valor numérico o de fecha está fuera del rango permitido (p. ej. `limit` fuera de `1..100`). ### parameter\_invalid\_cursor [#parameter_invalid_cursor] El cursor `starting_after` / `ending_before` no es un `id` de recurso válido. Consulta [Paginación](/guides/pagination). ### parameter\_unknown [#parameter_unknown] El body contiene un campo no documentado (en endpoints estrictos). ### invalid\_param\_format [#invalid_param_format] Falló una restricción de formato en un campo tipado — p. ej. el header `Idempotency-Key` o el header `Factuarea-Version` está malformado. ### invalid\_param\_value [#invalid_param_value] El valor no cumple una restricción (enum, formato, regla semántica). ### invalid\_period [#invalid_period] El periodo de reporte solicitado es inválido (p. ej. un trimestre/año que no existe). ### invalid\_status\_transition [#invalid_status_transition] La transición solicitada está prohibida por la máquina de estados del documento (p. ej. enviar una factura que no está en un estado enviable). Las violaciones de reglas de negocio como esta son `422`, no `409`. ### invoice\_already\_paid [#invoice_already_paid] `mark-paid` sobre una factura ya pagada. ### quote\_already\_accepted [#quote_already_accepted] Acción que entra en conflicto con un presupuesto ya aceptado. ### business\_rule\_violation [#business_rule_violation] Una invariante de dominio bloqueó la operación. El `subcode` identifica la regla y `param` el campo infractor. Lo usa el ledger de pagos ([Registrar pagos](/es/guides/payments)): * `payment_exceeds_pending_amount` (`param: "amount"`) — el importe del pago es mayor que el saldo pendiente de la factura. Aplica tanto a `POST /v1/invoices/{id}/payments` como a `POST /v1/purchase_invoices/{id}/payments`. * `invalid_payment_date` (`param: "paid_on"`) — la fecha de pago cae fuera de la ventana permitida `fecha_emisión … hoy` (facturas de compra). * `purchase_invoice_not_payable` (`param: "status"`) — la factura de compra está cancelada y ya no admite pagos. ```json { "error": { "type": "invalid_request_error", "code": "business_rule_violation", "subcode": "payment_exceeds_pending_amount", "message": "El importe del pago (1.500,00 €) supera el importe pendiente de la factura (710,00 €).", "param": "amount", "doc_url": "https://docs.factuarea.com/guides/errors#business_rule_violation", "request_id": "req_..." } } ``` ### unsupported\_format [#unsupported_format] El formato de exportación/reporte solicitado no está soportado. ### insufficient\_data\_for\_report [#insufficient_data_for_report] No hay datos suficientes para generar el reporte de impuestos solicitado. ### signature\_payload\_too\_large [#signature_payload_too_large] La imagen de firma del albarán supera el tamaño máximo. ### authentication\_error [#authentication_error] ### missing\_api\_key [#missing_api_key] No hay header de autenticación presente (`Authorization: Bearer` o `X-API-Key`). ### invalid\_api\_key [#invalid_api_key] La key no existe o el secreto no coincide con el hash almacenado. ### api\_key\_revoked [#api_key_revoked] La key fue revocada. Crea una nueva en el dashboard. ### too\_many\_auth\_failures [#too_many_auth_failures] Se han limitado fallos de autenticación repetidos desde tu cliente. Espera (back off) y verifica tus credenciales. ### payment\_required\_error [#payment_required_error] Todo `402` viene de una operación que cobra algo en el momento en que la llamas: un asiento de empresa gestionada, un asiento de empleado o un add-on. Ninguno se puede reintentar tal cual: resuelve antes el pago y repite la misma petición. <Callout type="info"> **Nota de versión.** Cinco de estos códigos se publicaron antes de que existiera esta categoría y se servían como `invalid_request_error`. Llevan `payment_required_error` desde [`Factuarea-Version: 2026-09-01`](/guides/versioning) en adelante: `payment_method_required`, `seat_charge_failed`, `gestoria_plan_required`, `employee_seat_payment_method_required` y `employee_seat_charge_failed`. Las peticiones en una versión anterior conservan el `type` de siempre. `error.code`, `error.subcode` y el estado `402` son idénticos en todas las versiones: ramifica por `code` y no tendrás que pensar en esto. </Callout> ### payment\_method\_required [#payment_method_required] `POST /v1/companies` y los endpoints de activación cobran un asiento de inmediato, y la gestoría opera en modo real sin método de pago configurado. La respuesta lleva `details.payment_setup_url`: ábrelo, registra una tarjeta y repite la llamada. ```json { "error": { "type": "payment_required_error", "code": "payment_method_required", "message": "La gestoría no tiene un método de pago configurado: configúralo para añadir la empresa.", "details": { "payment_setup_url": "https://billing.stripe.com/p/session/live_YWNjdF8xS2ZHM0RLb0h4RXBGV3lY" }, "request_id": "req_01HKQS5NPAYMENTMETHODREQ01", "doc_url": "https://docs.factuarea.com/guides/errors#payment_method_required" } } ``` ### seat\_charge\_failed [#seat_charge_failed] El cargo prorrateado del asiento de la empresa gestionada fue denegado —tarjeta rechazada, autenticación requerida, o el proveedor de pago inaccesible—. La empresa **no** se crea si el asiento no se cobra. Arregla el método de pago en el portal de facturación y reintenta. ### gestoria\_plan\_required [#gestoria_plan_required] La gestoría no tiene una suscripción de pago activa, así que no hay suscripción sobre la que cobrar el asiento. Contrata un plan (o reanuda el que canceló) antes de añadir empresas gestionadas. ### employee\_seat\_payment\_method\_required [#employee_seat_payment_method_required] Dar de alta o reactivar un empleado cobra un asiento de inmediato, y la empresa opera en modo real sin método de pago configurado. El remedio es el mismo que en `payment_method_required`, y la respuesta también lleva `details.payment_setup_url`. ### employee\_seat\_charge\_failed [#employee_seat_charge_failed] El cargo prorrateado del asiento de empleado fue denegado. El empleado **no** se activa si el asiento no se cobra. Arregla el método de pago y reintenta; consulta con tu banco si la tarjeta se sigue denegando. ### addon\_required [#addon_required] La operación pertenece a un add-on que la empresa no ha contratado: por ejemplo, `POST /v1/webhook_endpoints` requiere el add-on Developer API, cuyo nivel gratuito permite cero endpoints (`subcode: webhooks_addon_required`). Contrata el add-on y repite la llamada. A diferencia de [`addon_not_active`](#addon_not_active) (`403`), aquí lo que falta es la contratación, no el scope. ### authorization\_error [#authorization_error] ### insufficient\_scope [#insufficient_scope] La key no tiene el scope que requiere el endpoint. Consulta el catálogo en [Autenticación › Scopes](/guides/authentication#scopes). ### permission\_error [#permission_error] ### feature\_not\_available\_in\_plan [#feature_not_available_in_plan] El plan actual no incluye el módulo requerido (p. ej. `recurring_invoices`). ### addon\_not\_active [#addon_not_active] La empresa no tiene un plan de Factuarea activo que incluya acceso a la API pública — por ejemplo, el trial de 10 días caducó o la suscripción venció fuera de su periodo de gracia. Contrata o renueva un plan para seguir usando la API. ### not\_found\_error [#not_found_error] ### resource\_not\_found [#resource_not_found] El recurso no existe o no pertenece a tu empresa. ### tax\_report\_not\_found [#tax_report_not_found] El reporte de impuestos solicitado no existe. ### conflict\_error [#conflict_error] ### resource\_already\_exists [#resource_already_exists] Intento de crear un duplicado (p. ej. un `tax_id` ya registrado). El `subcode` (p. ej. `tax_id_already_exists`) señala la clave duplicada. ### resource\_conflict [#resource_conflict] La operación entra en conflicto con el estado actual del recurso (p. ej. una modificación concurrente). ### max\_api\_keys\_exceeded [#max_api_keys_exceeded] La empresa ha alcanzado su número máximo de API keys activas. ### idempotency\_error [#idempotency_error] ### idempotency\_key\_reused [#idempotency_key_reused] Mismo `Idempotency-Key`, body de petición distinto. Usa una key nueva. Consulta [Idempotencia](/guides/idempotency). ### rate\_limit\_error [#rate_limit_error] ### rate\_limit\_exceeded [#rate_limit_exceeded] Superaste la cuota por minuto o mensual de tu tier. El header `Retry-After` indica los segundos a esperar. Consulta [Límites de peticiones](/guides/rate-limits). ### api\_error [#api_error] ### internal\_error [#internal_error] Error inesperado. Ya está capturado por nuestra parte, pero comparte el `request_id` con soporte. ### service\_unavailable\_error [#service_unavailable_error] ### service\_unavailable [#service_unavailable] La API pública no está disponible temporalmente — deshabilitada globalmente vía kill-switch, en una ventana de mantenimiento, o una dependencia (base de datos, mailer, Stripe) no está sana. Reintenta tras un back-off corto. ## Errores tipados con el SDK oficial [#errores-tipados-con-el-sdk-oficial] Los [SDKs de TypeScript y PHP](/sdks) mapean este envoltorio a una jerarquía de excepciones tipada, así ramificas según una clase (y lees `code`, `type`, `param`, `request_id`) en vez de parsear JSON. Tu API key nunca se incluye en ninguna excepción. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { FactuareaError, ValidationError, RateLimitError, } from "@factuarea/sdk"; try { await factuarea.invoices.create(body); } catch (error) { if (error instanceof ValidationError) { console.error(error.fields); // { client_id: ["obligatorio"], … } } else if (error instanceof RateLimitError) { console.error(error.retryAfter); // seconds to wait } else if (error instanceof FactuareaError) { console.error(error.code, error.type, error.requestId); } } ``` La jerarquía también exporta `AuthenticationError`, `NotFoundError`, `ConflictError`, `ServerError` y `ConnectionError`. </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Models\Errors\ErrorThrowable; try { $factuarea->invoices->publicApiV1InvoicesCreate($body); } catch (ErrorThrowable $e) { $error = $e->container->error; echo $error->type->value; // e.g. "invalid_request_error" echo $error->code; // e.g. "parameter_invalid" echo $error->param; // e.g. "client_id" echo $error->requestId; // quote this to support } ``` </Tab> </Tabs> Consulta [SDKs › Gestión de errores](/sdks#handling-errors) para ver la jerarquía completa. La política de reintentos de abajo la aplican automáticamente ambos SDKs. ## request\_id y soporte [#request_id-y-soporte] Toda respuesta incluye un `request_id`. Adjúntalo a cualquier ticket o petición a `support@factuarea.com`: ``` Subject: 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV ``` Con el `request_id` correlacionamos logs, métricas y trazas para investigar rápido. ## Estrategia de reintentos [#estrategia-de-reintentos] * `4xx` excepto `429` → **no reintentes**: el error está en la petición. Corrígelo y reenvía. * `429` → respeta el header `Retry-After`. Implementa back-off exponencial con jitter. * `5xx` → back-off exponencial (`2^n * 100ms`) con jitter, máximo 5 intentos. Stripe publica un patrón canónico que también aplica aquí: [stripe.com/docs/error-handling](https://stripe.com/docs/error-handling). --- # Todos los error codes (/es/guides/errors/all) Esta es la referencia canónica de **todos** los `code` de error que puede devolver la API pública, agrupados por el bounded context que los emite. Cada `code` es estable entre versiones; el `message` es solo para mostrar. El total y la agrupación se generan del catálogo en vivo. ## Cuenta [#cuenta] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------ | | [`account_not_found`](/es/errors/account_not_found) | `not_found_error` | 404 | No se pudo resolver la cuenta asociada a la clave, lo que suele significar que la clave ya no apunta a una empresa viva. | | [`api_key_already_revoked`](/es/errors/api_key_already_revoked) | `invalid_request_error` | 422 | La clave ya estaba revocada, y una clave revocada no admite más operaciones: la revocación es terminal. | | [`api_key_not_found`](/es/errors/api_key_not_found) | `not_found_error` | 404 | El identificador no corresponde a ninguna API key de la empresa autenticada. | ## Autenticación [#autenticación] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ---------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`api_key_expired`](/es/errors/api_key_expired) | `authentication_error` | 401 | La clave pasó su fecha de caducidad. | | [`api_key_revoked`](/es/errors/api_key_revoked) | `authentication_error` | 401 | La clave fue revocada, y una clave revocada no vuelve a autenticar nunca: revocar es justamente la forma de cortar una credencial filtrada. | | [`invalid_api_key`](/es/errors/invalid_api_key) | `authentication_error` | 401 | La clave no corresponde a ninguna clave activa. Puede estar mal copiada, truncada, o pertenecer a otro entorno: las claves de prueba y las de producción no son intercambiables. | | [`ip_not_allowed`](/es/errors/ip_not_allowed) | `authentication_error` | 401 | La clave restringe las direcciones que acepta, y la petición llegó desde una que no está en esa lista. | | [`missing_api_key`](/es/errors/missing_api_key) | `authentication_error` | 401 | La petición no lleva credenciales: ni cabecera `Authorization` ni `X-API-Key`. | | [`origin_not_allowed`](/es/errors/origin_not_allowed) | `authentication_error` | 401 | La petición viene de un origen de navegador que la clave no acepta. | | [`too_many_auth_failures`](/es/errors/too_many_auth_failures) | `authentication_error` | 429 | Llegaron demasiados intentos fallidos de autenticación desde la misma dirección, así que queda bloqueada temporalmente para frenar los intentos de adivinar credenciales. | ## Autorización [#autorización] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------- | --------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_not_active`](/es/errors/addon_not_active) | `authorization_error` | 403 | La funcionalidad pertenece a un add-on que ahora mismo no está activo para la empresa. | | [`feature_not_available_in_plan`](/es/errors/feature_not_available_in_plan) | `authorization_error` | 403 | La funcionalidad no está incluida en el plan de la empresa. | | [`forbidden_action`](/es/errors/forbidden_action) | `authorization_error` | 403 | La acción está bloqueada para este recurso aunque el scope sea el correcto: el recurso pertenece a un catálogo compartido, o el cambio va por otro endpoint. | | [`insufficient_scope`](/es/errors/insufficient_scope) | `authorization_error` | 403 | La clave autentica correctamente pero no lleva el scope que exige esta operación. Los scopes se conceden al emitir la clave y no se amplían en tiempo de llamada. | | [`max_api_keys_exceeded`](/es/errors/max_api_keys_exceeded) | `authorization_error` | 422 | La empresa alcanzó el número de API keys que permite su plan. | | [`max_webhook_endpoints_exceeded`](/es/errors/max_webhook_endpoints_exceeded) | `authorization_error` | 422 | La empresa alcanzó el número de endpoints de webhook que permite su nivel de add-on. | | [`module_not_available_in_sandbox`](/es/errors/module_not_available_in_sandbox) | `authorization_error` | 403 | El recurso pertenece a un módulo vetado en modo test. La sandbox nunca toca AEAT, bancos ni cobros reales, así que esos módulos quedan fuera a propósito. | | [`scope_not_allowed_by_plan`](/es/errors/scope_not_allowed_by_plan) | `authorization_error` | 422 | Uno de los scopes pedidos pertenece a un módulo que el plan no incluye, así que la clave nacería con un permiso que nunca podría ejercer. | | [`scope_not_allowed_in_sandbox`](/es/errors/scope_not_allowed_in_sandbox) | `authorization_error` | 422 | Una clave de prueba no puede nacer con scopes de módulos vetados en sandbox. | ## Clientes [#clientes] | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alternative_id_type_invalid`](/es/errors/alternative_id_type_invalid) | `invalid_request_error` | 422 | El tipo de identificador alternativo queda fuera del catálogo `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. | | [`cannot_have_both_tax_id_and_alternative_id`](/es/errors/cannot_have_both_tax_id_and_alternative_id) | `invalid_request_error` | 422 | El cliente envía `tax_id` y un identificador alternativo a la vez. La identidad fiscal es una: el identificador alternativo existe precisamente para partes sin NIF español. | | [`census_requires_tax_id`](/es/errors/census_requires_tax_id) | `invalid_request_error` | 422 | La verificación censal contrasta el par nombre + NIF contra la AEAT, y falta uno de los dos. | | [`client_has_documents`](/es/errors/client_has_documents) | `invalid_request_error` | 422 | El cliente está referenciado por documentos emitidos. Borrarlo dejaría facturas, presupuestos o albaranes sin la parte a la que se emitieron, y los registros fiscales tienen que seguir siendo trazables. | | [`client_import_too_large`](/es/errors/client_import_too_large) | `invalid_request_error` | 422 | El CSV supera el límite de filas que admite la importación síncrona, ya que el fichero entero se procesa dentro de la propia petición. | | [`client_not_found`](/es/errors/client_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún cliente de la empresa autenticada. | | [`client_requires_tax_identity`](/es/errors/client_requires_tax_identity) | `invalid_request_error` | 422 | El cliente no tiene identidad fiscal: ni `tax_id` ni identificador alternativo, y no se puede emitir una factura a una parte sin identificar. | | [`direct_debit_requires_default_bank_account`](/es/errors/direct_debit_requires_default_bank_account) | `invalid_request_error` | 422 | Se eligió domiciliación bancaria como método de pago, pero el cliente no tiene cuenta bancaria por defecto a la que cargar. | | [`tax_id_already_exists`](/es/errors/tax_id_already_exists) | `conflict_error` | 409 | Otro cliente de la empresa ya tiene ese NIF, y el NIF identifica a la parte sin ambigüedad dentro de una empresa. | ## Empresas [#empresas] | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------- | ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`company_inactive`](/es/errors/company_inactive) | `authorization_error` | 403 | El perfil que indica `X-Active-Profile` es una de tus empresas gestionadas, pero está desactivada y no se puede operar hasta que vuelva a estar activa. | | [`gestoria_module_required`](/es/errors/gestoria_module_required) | `authorization_error` | 403 | La gestoría tiene un plan vigente, pero sin el módulo de gestoría, así que no puede crear ni operar empresas gestionadas. | | [`gestoria_plan_required`](/es/errors/gestoria_plan_required) | `payment_required_error` | 402 | La gestoría no tiene una suscripción de pago activa, así que no hay suscripción sobre la que cobrar el asiento. | | [`payment_method_required`](/es/errors/payment_method_required) | `payment_required_error` | 402 | Dar de alta una empresa gestionada cobra un asiento de inmediato, y la gestoría opera en modo real sin método de pago configurado. | | [`seat_charge_failed`](/es/errors/seat_charge_failed) | `payment_required_error` | 402 | El cobro inmediato del prorrateo del asiento fue rechazado: la tarjeta se denegó, necesita autenticación, o el proveedor de pago estaba inaccesible. La empresa no se crea si el asiento no se cobra. | ## Albaranes [#albaranes] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------ | | [`delivery_note_not_found`](/es/errors/delivery_note_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún albarán de la empresa autenticada. | | [`delivery_note_section_not_editable_in_status`](/es/errors/delivery_note_section_not_editable_in_status) | `invalid_request_error` | 422 | La sección logística —transportista, vehículo, conductor— está congelada porque el albarán ya está entregado, facturado o cancelado. | | [`driver_tax_id_requires_name`](/es/errors/driver_tax_id_requires_name) | `invalid_request_error` | 422 | Se envió el NIF del conductor sin su nombre, y un identificador sin nombre no identifica a nadie en el documento de entrega. | | [`signature_payload_too_large`](/es/errors/signature_payload_too_large) | `invalid_request_error` | 422 | La imagen de la firma supera el tamaño admitido para el campo. | ## Empleados [#empleados] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------------------- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`employee_seat_charge_failed`](/es/errors/employee_seat_charge_failed) | `payment_required_error` | 402 | El cobro inmediato del prorrateo del asiento de empleado fue rechazado: la tarjeta se denegó, necesita autenticación, o el proveedor de pago estaba inaccesible. El empleado no se activa si el asiento no se cobra. | | [`employee_seat_payment_method_required`](/es/errors/employee_seat_payment_method_required) | `payment_required_error` | 402 | Dar de alta o reactivar un empleado cobra un asiento de inmediato, y la empresa opera en modo real sin método de pago configurado. | ## Events [#events] | Code | Type | HTTP | Descripción | | ----------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [`event_not_found`](/es/errors/event_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún evento de la empresa autenticada, o el evento fue purgado por la política de retención de 30 días. | ## Idempotency [#idempotency] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`idempotency_key_in_use`](/es/errors/idempotency_key_in_use) | `idempotency_error` | 409 | Hay otra petición con la misma `Idempotency-Key` todavía en curso, y aún no se conoce su resultado. | | [`idempotency_key_invalid`](/es/errors/idempotency_key_invalid) | `invalid_request_error` | 400 | La `Idempotency-Key` no encaja con el formato admitido: entre 1 y 255 caracteres ASCII imprimibles. | | [`idempotency_key_reused`](/es/errors/idempotency_key_reused) | `idempotency_error` | 409 | Esa `Idempotency-Key` ya se usó con un payload distinto. La clave identifica una operación concreta, así que reutilizarla para otra vaciaría de sentido el replay. | ## Facturas [#facturas] | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`corrective_invoice_inanulable`](/es/errors/corrective_invoice_inanulable) | `invalid_request_error` | 422 | La factura es a su vez una rectificativa, y las rectificativas nunca se anulan: la cadena de corrección tiene que seguir siendo auditable de punta a punta. | | [`export_limit_exceeded`](/es/errors/export_limit_exceeded) | `invalid_request_error` | 422 | La selección filtrada supera el tope de 5.000 facturas de la exportación, así que el fichero se rechaza de entrada en lugar de truncarse en silencio. | | [`invalid_correction_nature`](/es/errors/invalid_correction_nature) | `invalid_request_error` | 422 | `correction_nature` solo acepta `S` (sustitución: la rectificativa lleva los importes corregidos completos) o `I` (por diferencias: lleva solo el delta). | | [`invalid_correction_reason`](/es/errors/invalid_correction_reason) | `invalid_request_error` | 422 | El motivo de rectificación queda fuera de la lista fiscal cerrada (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), que mapea a los códigos AEAT R1 a R4. | | [`invalid_invoice_id`](/es/errors/invalid_invoice_id) | `invalid_request_error` | 400 | La referencia de factura recibida no es un identificador válido; suele significar que se coló un valor interno donde la API espera el `id` público. | | [`invalid_invoice_number`](/es/errors/invalid_invoice_number) | `invalid_request_error` | 422 | El número de factura no sigue el formato canónico `SERIE-AAAA-NNN`, más el sufijo `-RECn` en las rectificativas. | | [`invalid_invoice_status`](/es/errors/invalid_invoice_status) | `invalid_request_error` | 422 | El valor enviado como estado de factura queda fuera del catálogo del ciclo de vida (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). | | [`invalid_invoice_uuid`](/es/errors/invalid_invoice_uuid) | `invalid_request_error` | 400 | El identificador de factura de la ruta o del payload no es un UUID válido. | | [`invalid_payment_method`](/es/errors/invalid_payment_method) | `invalid_request_error` | 422 | El método de pago queda fuera de la allowlist cerrada: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. | | [`invoice_already_annulled`](/es/errors/invoice_already_annulled) | `invalid_request_error` | 422 | La factura ya estaba anulada. La anulación es terminal y, con VeriFactu activo, su registro de anulación ya llegó a la AEAT. | | [`invoice_already_paid`](/es/errors/invoice_already_paid) | `invalid_request_error` | 422 | La factura ya está cobrada. `paid` es un estado terminal y contablemente cerrado: el IVA repercutido ya se ha declarado, o se declarará en el período. | | [`invoice_already_sent`](/es/errors/invoice_already_sent) | `invalid_request_error` | 422 | La factura ya fue emitida: tiene número definitivo de serie y, con VeriFactu activo, su alta en la AEAT. La emisión no ocurre dos veces. | | [`invoice_cannot_assign_number`](/es/errors/invoice_cannot_assign_number) | `invalid_request_error` | 422 | Se pidió número definitivo para una factura que no es borrador, o que ya lo tiene. La numeración de serie es monótona y los números no se reasignan. | | [`invoice_invalid_status_transition`](/es/errors/invoice_invalid_status_transition) | `invalid_request_error` | 422 | El estado destino no es alcanzable desde el actual. El ciclo de vida es dirigido: `draft` pasa a `scheduled` o `sent`, `sent` a `paid`, `overdue` o `annulled`, y `paid`, `cancelled` y `annulled` son terminales. | | [`invoice_not_cancellable_in_current_state`](/es/errors/invoice_not_cancellable_in_current_state) | `invalid_request_error` | 422 | Cancelar retira un borrador que todavía no es fiscalmente vinculante, así que solo aplica mientras la factura está en `draft`. | | [`invoice_not_correctable_in_current_state`](/es/errors/invoice_not_correctable_in_current_state) | `invalid_request_error` | 422 | Una rectificativa solo se emite contra una factura ya emitida (`sent` o `paid`). Un borrador, una factura cancelada o una anulada no tienen nada que rectificar. | | [`invoice_not_deletable_in_current_state`](/es/errors/invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Solo se borran las facturas en `draft` y `cancelled`. Una factura numerada nunca desaparece: la serie correlativa debe seguir siendo auditable. | | [`invoice_not_editable_in_current_state`](/es/errors/invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Solo un borrador admite edición. Una vez emitida, la factura es inmutable y su contenido queda congelado junto con su registro fiscal. | | [`invoice_not_eligible_for_action`](/es/errors/invoice_not_eligible_for_action) | `invalid_request_error` | 422 | La acción solicitada no aplica a esta factura: su tipo o su estado actual la dejan fuera del alcance de la operación. | | [`invoice_not_found`](/es/errors/invoice_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna factura de la empresa autenticada. Las facturas de otra empresa responden exactamente igual. | | [`invoice_not_modifiable_in_current_state`](/es/errors/invoice_not_modifiable_in_current_state) | `invalid_request_error` | 422 | El campo que intentas cambiar está congelado para el estado actual — por ejemplo el régimen fiscal de una factura anulada. | | [`invoice_not_paid`](/es/errors/invoice_not_paid) | `invalid_request_error` | 422 | Se pidió un justificante de pago de una factura sin cobro registrado, así que no hay nada que certificar. | | [`invoice_not_reschedulable_in_current_state`](/es/errors/invoice_not_reschedulable_in_current_state) | `invalid_request_error` | 422 | Reprogramar mueve la fecha de emisión de una factura que está esperando en `scheduled`, y esta factura no está esperando. | | [`invoice_not_schedulable_in_current_state`](/es/errors/invoice_not_schedulable_in_current_state) | `invalid_request_error` | 422 | Solo un borrador se puede programar: la programación reserva un momento futuro de emisión sin consumir todavía número de serie. | | [`invoice_not_unschedulable_in_current_state`](/es/errors/invoice_not_unschedulable_in_current_state) | `invalid_request_error` | 422 | Desprogramar devuelve la factura de `scheduled` a `draft`, así que solo aplica mientras sigue esperando a emitirse. | | [`invoice_not_unsendable_in_current_state`](/es/errors/invoice_not_unsendable_in_current_state) | `invalid_request_error` | 422 | Deshacer la marca de entrega solo aplica a una factura `sent`: limpia `sent_at` y mantiene la factura emitida. | | [`invoice_requires_at_least_one_line`](/es/errors/invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura no lleva ninguna línea de operación, así que no tiene base imponible y no se puede emitir. Ocurre cuando no envías líneas y cuando todas las que envías son de suplido: un suplido es una cantidad pagada por cuenta del cliente (art. 78.Tres.3 LIVA), no una operación tuya. | | [`invoice_year_required_for_ambiguous_number`](/es/errors/invoice_year_required_for_ambiguous_number) | `invalid_request_error` | 422 | Ese número de factura existe en más de un ejercicio, así que por sí solo no identifica una única factura. | | [`line_total_checksum_mismatch`](/es/errors/line_total_checksum_mismatch) | `invalid_request_error` | 422 | El `line_total` declarado no coincide con el que calcula Factuarea para esa línea (cantidad × precio − descuento + IVA − retención + recargo) y la desviación supera el céntimo de tolerancia. El importe que se factura y se declara a la AEAT es siempre el calculado aquí, así que la discrepancia significa que tu sistema y la factura emitida no cuadrarían. | | [`line_type_invalid`](/es/errors/line_type_invalid) | `invalid_request_error` | 422 | El tipo de línea queda fuera del catálogo cerrado `NORMAL` / `SUPLIDO`. Una factura emitida sólo distingue dos naturalezas: lo que vendes tú, que forma base imponible y lleva IVA, y el suplido, que es dinero adelantado en nombre y por cuenta del cliente y por eso queda fuera de la base (art. 78.Tres.3 LIVA). | | [`no_invoices_in_period`](/es/errors/no_invoices_in_period) | `invalid_request_error` | 422 | La operación trimestral no encontró facturas en el período pedido, así que no hay nada que empaquetar ni enviar. | | [`payment_method_invalid`](/es/errors/payment_method_invalid) | `invalid_request_error` | 422 | La misma allowlist cerrada que `invalid_payment_method`, reportada cuando el valor se rechaza al leer el campo de método de pago del payload. | | [`reminder_not_applicable`](/es/errors/reminder_not_applicable) | `invalid_request_error` | 422 | El recordatorio de pago no procede: la factura no está en `sent` ni `overdue`, no hay email de destinatario, falta el enlace público o está desactivado, o ya salió otro recordatorio en las últimas 24 horas. | | [`scheduled_for_in_past`](/es/errors/scheduled_for_in_past) | `invalid_request_error` | 422 | `scheduled_for` no es estrictamente futuro, así que no hay ninguna espera que reservar. | | [`simplified_invoice_cannot_be_substituted`](/es/errors/simplified_invoice_cannot_be_substituted) | `invalid_request_error` | 422 | Una de las facturas de la lista de sustitución no se puede sustituir: no es simplificada, está cancelada o anulada, pertenece a otra empresa, o ya tiene sustitutiva. | | [`simplified_invoice_not_allowed`](/es/errors/simplified_invoice_not_allowed) | `invalid_request_error` | 422 | La operación no es elegible para factura simplificada: supera los 3.000 €, o es una entrega intracomunitaria, una exportación, una operación con inversión del sujeto pasivo, o el cliente necesita factura completa para deducir el IVA. | | [`simplified_limit_exceeded`](/es/errors/simplified_limit_exceeded) | `invalid_request_error` | 422 | Las líneas llevarían la factura simplificada (F2) por encima del tope legal absoluto de 3.000 € IVA incluido. | | [`suplido_line_cannot_carry_taxes`](/es/errors/suplido_line_cannot_carry_taxes) | `invalid_request_error` | 422 | La línea de suplido lleva carga propia: tipo de IVA, retención, recargo de equivalencia, descuento, clave de régimen, causa de exención o producto/pack. Un suplido no es una operación del emisor, así que repercutir un impuesto sobre él sería tributar por una entrega que no has hecho, y ligarlo a un producto movería un stock que nunca has vendido. | | [`suplido_not_allowed_in_simplified_invoice`](/es/errors/suplido_not_allowed_in_simplified_invoice) | `invalid_request_error` | 422 | La factura es simplificada (F2) y una simplificada no identifica al destinatario. Sin destinatario identificado no hay a quién acreditar el pago por cuenta ajena, así que el importe no admite el tratamiento de suplido en este tipo de factura. | | [`suplido_requires_source_invoice_reference`](/es/errors/suplido_requires_source_invoice_reference) | `invalid_request_error` | 422 | La línea de suplido no informa `source_invoice_reference`, el número del justificante que el tercero expidió a nombre del cliente. Sin ese justificante el pago no se acredita como hecho por cuenta ajena y Hacienda lo trataría como base imponible propia del emisor, con su IVA repercutido. | ## Notificaciones [#notificaciones] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [`notification_not_found`](/es/errors/notification_not_found) | `not_found_error` | 404 | El identificador no corresponde a ninguna notificación de la empresa autenticada, o la notificación quedó fuera de la ventana de retención. | ## Pagos [#pagos] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_payment_date`](/es/errors/invalid_payment_date) | `invalid_request_error` | 422 | La fecha de pago queda fuera de la ventana admitida: no puede ser anterior a la fecha de emisión de la factura ni situarse en el futuro. | | [`payout_reconciliation_amount_mismatch`](/es/errors/payout_reconciliation_amount_mismatch) | `invalid_request_error` | 422 | El importe confirmado no coincide con el neto de la liquidación, así que la conciliación cerraría con una diferencia que nadie justifica. | | [`receipt_not_available`](/es/errors/receipt_not_available) | `invalid_request_error` | 422 | No hay justificante que emitir porque el documento no tiene ningún cobro registrado detrás. | | [`stripe_payout_already_reconciled`](/es/errors/stripe_payout_already_reconciled) | `invalid_request_error` | 422 | La liquidación ya estaba conciliada, y la conciliación es terminal: repetirla contabilizaría dos veces el apunte bancario. | | [`stripe_payout_not_found`](/es/errors/stripe_payout_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna liquidación de la empresa autenticada. | ## Productos [#productos] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------- | | [`pack_in_use`](/es/errors/pack_in_use) | `invalid_request_error` | 422 | El pack está referenciado por documentos emitidos, así que borrarlo rompería su composición. | | [`pack_not_found`](/es/errors/pack_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún pack de la empresa autenticada. | | [`pack_share_link_failed`](/es/errors/pack_share_link_failed) | `api_error` | 500 | No se pudo generar el enlace para compartir el pack. El pack en sí no queda afectado. | | [`product_in_use`](/es/errors/product_in_use) | `invalid_request_error` | 422 | El producto está referenciado por documentos emitidos o por otras entradas del catálogo, y eliminarlo dejaría esas referencias colgando. | | [`product_not_found`](/es/errors/product_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún producto de la empresa autenticada. | | [`sku_already_exists`](/es/errors/sku_already_exists) | `conflict_error` | 409 | Otro producto de la empresa ya usa ese SKU, y el SKU identifica al artículo sin ambigüedad dentro del catálogo. | ## Facturas proforma [#facturas-proforma] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`invalid_expiry_date`](/es/errors/invalid_expiry_date) | `invalid_request_error` | 422 | La fecha de vencimiento es anterior a la de emisión, o la supera en más de 365 días. | | [`invalid_proforma_id`](/es/errors/invalid_proforma_id) | `invalid_request_error` | 400 | La referencia de proforma recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. | | [`invalid_proforma_number`](/es/errors/invalid_proforma_number) | `invalid_request_error` | 422 | El número de proforma no sigue el formato canónico de numeración de su serie. | | [`invalid_proforma_status`](/es/errors/invalid_proforma_status) | `invalid_request_error` | 422 | El valor enviado como estado queda fuera del catálogo `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. | | [`invalid_proforma_uuid`](/es/errors/invalid_proforma_uuid) | `invalid_request_error` | 400 | El identificador de proforma de la ruta o del payload no es un UUID válido. | | [`proforma_already_accepted`](/es/errors/proforma_already_accepted) | `invalid_request_error` | 422 | El cliente ya aceptó la proforma, y la aceptación se registra una sola vez. | | [`proforma_already_rejected`](/es/errors/proforma_already_rejected) | `invalid_request_error` | 422 | La proforma ya está marcada como rechazada. | | [`proforma_cannot_be_accepted`](/es/errors/proforma_cannot_be_accepted) | `invalid_request_error` | 422 | La aceptación no procede desde el estado actual: una proforma facturada, cancelada o expirada ya no la admite. | | [`proforma_cannot_be_rejected`](/es/errors/proforma_cannot_be_rejected) | `invalid_request_error` | 422 | El rechazo no procede desde el estado actual: una vez facturada, cancelada o expirada, la proforma está cerrada. | | [`proforma_cannot_be_sent`](/es/errors/proforma_cannot_be_sent) | `invalid_request_error` | 422 | El envío por email no aplica a una proforma en estado terminal: no hay oferta viva que entregar. | | [`proforma_invalid_status_transition`](/es/errors/proforma_invalid_status_transition) | `invalid_request_error` | 422 | El estado destino no es alcanzable desde el actual: un borrador se acepta, se cancela o expira; una proforma aceptada se factura, se rechaza o expira; facturada, cancelada y expirada son terminales. | | [`proforma_not_convertible_in_current_state`](/es/errors/proforma_not_convertible_in_current_state) | `invalid_request_error` | 422 | Convertir en factura exige que el cliente haya aceptado la proforma; desde cualquier otro estado no hay acuerdo que facturar. | | [`proforma_not_deletable_in_current_state`](/es/errors/proforma_not_deletable_in_current_state) | `invalid_request_error` | 422 | Solo se borra una proforma en borrador. Una vez aceptada, rechazada o facturada forma parte del rastro comercial. | | [`proforma_not_draft`](/es/errors/proforma_not_draft) | `invalid_request_error` | 422 | La operación solo tiene sentido mientras la proforma es un borrador, y esta ya ha avanzado. | | [`proforma_not_editable_in_current_state`](/es/errors/proforma_not_editable_in_current_state) | `invalid_request_error` | 422 | Solo una proforma en borrador admite edición. Una vez aceptada, rechazada, expirada, facturada o cancelada, su contenido queda fijado. | | [`proforma_not_found`](/es/errors/proforma_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna proforma de la empresa autenticada. | | [`proforma_requires_at_least_one_line`](/es/errors/proforma_requires_at_least_one_line) | `invalid_request_error` | 422 | La proforma no lleva líneas, así que no hay importe que poner delante del cliente. | | [`public_link_expires_at_exceeds_max_days`](/es/errors/public_link_expires_at_exceeds_max_days) | `invalid_request_error` | 422 | La caducidad pedida para el enlace público supera la ventana máxima que permite tu plan para documentos compartidos. | ## Facturas de compra [#facturas-de-compra] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`attachment_invalid_filename`](/es/errors/attachment_invalid_filename) | `invalid_request_error` | 422 | El nombre del fichero no es utilizable: está vacío, lleva componentes de ruta, o supera los 200 caracteres. | | [`attachment_mime_not_allowed`](/es/errors/attachment_mime_not_allowed) | `invalid_request_error` | 422 | El tipo de fichero queda fuera del conjunto admitido: PDF, PNG, JPEG, XML y HTML. | | [`attachment_missing`](/es/errors/attachment_missing) | `not_found_error` | 404 | La factura de compra existe pero no tiene fichero adjunto, así que no hay nada que descargar. | | [`attachment_too_large`](/es/errors/attachment_too_large) | `invalid_request_error` | 422 | El fichero supera el tamaño máximo permitido para un adjunto de documento. | | [`cannot_attach_to_cancelled_purchase_invoice`](/es/errors/cannot_attach_to_cancelled_purchase_invoice) | `invalid_request_error` | 422 | La factura está cancelada, y adjuntar documentos a un registro cancelado alteraría documentación ya cerrada. | | [`invalid_purchase_invoice_id`](/es/errors/invalid_purchase_invoice_id) | `invalid_request_error` | 400 | La referencia de factura de compra recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. | | [`invalid_purchase_invoice_number`](/es/errors/invalid_purchase_invoice_number) | `invalid_request_error` | 422 | El número de factura está vacío o no encaja con el formato admitido. En una factura de compra el número es el que imprimió el proveedor, no uno que genere Factuarea. | | [`invalid_purchase_invoice_uuid`](/es/errors/invalid_purchase_invoice_uuid) | `invalid_request_error` | 400 | El identificador de factura de compra de la ruta o del payload no es un UUID válido. | | [`operation_regime_invalid`](/es/errors/operation_regime_invalid) | `invalid_request_error` | 422 | El régimen de operación queda fuera del catálogo `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. | | [`purchase_invoice_already_exists`](/es/errors/purchase_invoice_already_exists) | `conflict_error` | 409 | Ese proveedor ya tiene registrada una factura de compra con el mismo número. El par proveedor + número identifica el documento sin ambigüedad y evita contabilizar dos veces el mismo gasto. | | [`purchase_invoice_not_deletable_in_current_state`](/es/errors/purchase_invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Solo se borran las facturas de compra en borrador o canceladas. Una pendiente o pagada forma parte del libro de gastos. | | [`purchase_invoice_not_draft`](/es/errors/purchase_invoice_not_draft) | `invalid_request_error` | 422 | La operación solo aplica mientras la factura de compra es un borrador, y esta ya está registrada. | | [`purchase_invoice_not_editable_in_current_state`](/es/errors/purchase_invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Solo se edita una factura de compra en borrador. Una vez registrada como pendiente, pagada o cancelada, su contenido respalda un apunte contable. | | [`purchase_invoice_not_found`](/es/errors/purchase_invoice_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna factura de compra de la empresa autenticada. | | [`purchase_invoice_requires_at_least_one_line`](/es/errors/purchase_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La factura de compra no lleva líneas, así que no hay gasto ni IVA soportado que registrar. | ## Presupuestos [#presupuestos] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------- | | [`quote_already_accepted`](/es/errors/quote_already_accepted) | `invalid_request_error` | 422 | El presupuesto ya estaba aprobado, y la aprobación se registra una sola vez. | | [`quote_already_rejected`](/es/errors/quote_already_rejected) | `invalid_request_error` | 422 | El presupuesto ya está marcado como rechazado. | | [`quote_expired`](/es/errors/quote_expired) | `invalid_request_error` | 422 | El presupuesto pasó su fecha de validez, así que las condiciones ofrecidas ya no vinculan y no se puede aprobar ni convertir tal cual. | | [`quote_not_found`](/es/errors/quote_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún presupuesto de la empresa autenticada. | ## Límite de tasa [#límite-de-tasa] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ------------------ | ---- | ------------------------------------------------------------------------------- | | [`monthly_quota_exceeded`](/es/errors/monthly_quota_exceeded) | `rate_limit_error` | 429 | La empresa agotó la cuota mensual de llamadas que incluye su plan. | | [`rate_limit_exceeded`](/es/errors/rate_limit_exceeded) | `rate_limit_error` | 429 | La clave envió más peticiones de las que permite su ritmo en la ventana actual. | ## Facturas recurrentes [#facturas-recurrentes] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_frequency_interval`](/es/errors/invalid_frequency_interval) | `invalid_request_error` | 422 | El intervalo es menor que 1, así que la recurrencia nunca avanzaría a una siguiente ejecución. | | [`invalid_frequency_type`](/es/errors/invalid_frequency_type) | `invalid_request_error` | 422 | La frecuencia queda fuera del catálogo `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. | | [`invalid_holiday_handling`](/es/errors/invalid_holiday_handling) | `invalid_request_error` | 422 | La política de festivos queda fuera del catálogo `skip`, `before`, `after`, `same`. | | [`invalid_recurring_invoice_id`](/es/errors/invalid_recurring_invoice_id) | `invalid_request_error` | 400 | La referencia de recurrencia recibida no es un identificador válido, normalmente porque un valor interno sustituyó al `id` público. | | [`invalid_recurring_invoice_uuid`](/es/errors/invalid_recurring_invoice_uuid) | `invalid_request_error` | 400 | El identificador de recurrencia de la ruta o del payload no es un UUID válido. | | [`recurring_already_active`](/es/errors/recurring_already_active) | `invalid_request_error` | 422 | La recurrencia ya está en marcha, así que no hay nada que activar. Código legacy conservado por compatibilidad: los endpoints actuales reportan esto como `recurring_invoice_already_active`. | | [`recurring_invoice_already_active`](/es/errors/recurring_invoice_already_active) | `invalid_request_error` | 422 | La recurrencia ya está en marcha. | | [`recurring_invoice_already_cancelled`](/es/errors/recurring_invoice_already_cancelled) | `invalid_request_error` | 422 | La recurrencia ya estaba cancelada, y la cancelación es terminal. | | [`recurring_invoice_already_paused`](/es/errors/recurring_invoice_already_paused) | `invalid_request_error` | 422 | La recurrencia ya está pausada, así que pausarla otra vez no cambia nada. | | [`recurring_invoice_cancelled_cannot_resume`](/es/errors/recurring_invoice_cancelled_cannot_resume) | `invalid_request_error` | 422 | Una recurrencia cancelada no se reanuda: la cancelación la cierra definitivamente, a diferencia de la pausa. | | [`recurring_invoice_cannot_run`](/es/errors/recurring_invoice_cannot_run) | `invalid_request_error` | 422 | La recurrencia no puede generar una factura ahora mismo: no está en marcha, su ciclo terminó, o le faltan datos que la factura necesita. `error.message` indica el motivo concreto. | | [`recurring_invoice_has_generated_invoices`](/es/errors/recurring_invoice_has_generated_invoices) | `invalid_request_error` | 422 | La recurrencia ya generó facturas, y esas facturas dependen de ella para su trazabilidad. | | [`recurring_invoice_not_found`](/es/errors/recurring_invoice_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna recurrencia de la empresa autenticada. | | [`recurring_invoice_requires_at_least_one_line`](/es/errors/recurring_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | La recurrencia no lleva líneas, así que cada factura generada saldría vacía. | | [`recurring_not_active`](/es/errors/recurring_not_active) | `invalid_request_error` | 422 | La operación necesita una recurrencia en marcha y esta está pausada, completada o cancelada. Código legacy conservado por compatibilidad con integraciones antiguas. | ## Request [#request] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`business_rule_violation`](/es/errors/business_rule_violation) | `invalid_request_error` | 422 | Una invariante del dominio rechazó la operación. Este código indica la familia; `error.subcode` nombra la regla concreta y `error.message` la explica. | | [`conflicting_pagination_params`](/es/errors/conflicting_pagination_params) | `invalid_request_error` | 422 | `starting_after` y `ending_before` viajaron en la misma petición. Recorren la colección en sentidos opuestos, así que solo puede aplicarse uno. | | [`external_id_already_exists`](/es/errors/external_id_already_exists) | `conflict_error` | 409 | El `external_id` con el que concilias contra tu sistema ya está asignado a otro objeto del mismo tipo en esta empresa. | | [`invalid_param_format`](/es/errors/invalid_param_format) | `invalid_request_error` | 422 | Un form request legacy rechazó la forma de un valor. Los endpoints migrados reportan lo mismo como `parameter_invalid_format` o `parameter_invalid_integer`. | | [`invalid_param_value`](/es/errors/invalid_param_value) | `invalid_request_error` | 422 | Un form request legacy rechazó el valor de un campo. Los endpoints migrados reportan lo mismo como `parameter_invalid_enum` o `parameter_invalid_range`. | | [`invalid_status_transition`](/es/errors/invalid_status_transition) | `invalid_request_error` | 422 | El estado solicitado no es alcanzable desde el estado en el que está ahora mismo el documento. | | [`length_required`](/es/errors/length_required) | `invalid_request_error` | 411 | Llegó una petición con body en codificación chunked, sin declarar su tamaño. La API necesita conocer la longitud por adelantado para rechazar payloads excesivos antes de cargarlos en memoria. | | [`metadata_too_many_keys`](/es/errors/metadata_too_many_keys) | `invalid_request_error` | 422 | El objeto `metadata` supera el límite de 50 claves por recurso. | | [`metadata_value_too_long`](/es/errors/metadata_value_too_long) | `invalid_request_error` | 422 | Un valor de `metadata` supera los 500 caracteres una vez serializado a texto. | | [`method_not_allowed`](/es/errors/method_not_allowed) | `invalid_request_error` | 405 | La ruta existe pero no acepta el verbo HTTP utilizado. | | [`missing_required_param`](/es/errors/missing_required_param) | `invalid_request_error` | 422 | Un form request legacy detectó que faltaba un campo obligatorio. Los endpoints ya migrados a los parsers canónicos reportan lo mismo como `parameter_missing`. | | [`parameter_invalid`](/es/errors/parameter_invalid) | `invalid_request_error` | 422 | Un value object construido a partir del payload rechazó el valor recibido. `error.subcode` dice cuál: código de impuesto, código de país, tipo impositivo, etc. | | [`parameter_invalid_boolean`](/es/errors/parameter_invalid_boolean) | `invalid_request_error` | 400 | Un parámetro que debe ser booleano recibió un valor fuera de las representaciones aceptadas (`true`/`false`, `1`/`0`). | | [`parameter_invalid_cursor`](/es/errors/parameter_invalid_cursor) | `invalid_request_error` | 400 | El cursor `starting_after` o `ending_before` no es un UUID válido, así que no puede apuntar a ninguna fila de la colección. | | [`parameter_invalid_empty`](/es/errors/parameter_invalid_empty) | `invalid_request_error` | 400 | Un parámetro llegó con el valor vacío: un filtro `in` sin elementos, una comparación sin nada tras el operador, o un filtro de igualdad con la cadena vacía. | | [`parameter_invalid_enum`](/es/errors/parameter_invalid_enum) | `invalid_request_error` | 400 | El valor queda fuera del conjunto cerrado que acepta el parámetro. En los listados cubre además un operador de filtro distinto de `eq`, `gte`, `lte`, `gt`, `lt`, `in` o `contains`. | | [`parameter_invalid_format`](/es/errors/parameter_invalid_format) | `invalid_request_error` | 400 | El valor tiene el tipo correcto pero no la forma que exige el parámetro: una fecha, un patrón de identificador o una cabecera como `Factuarea-Version`. | | [`parameter_invalid_integer`](/es/errors/parameter_invalid_integer) | `invalid_request_error` | 400 | Un parámetro que debe ser un número entero recibió algo que no se puede interpretar como tal, por ejemplo `limit=abc`. | | [`parameter_invalid_iso8601`](/es/errors/parameter_invalid_iso8601) | `invalid_request_error` | 400 | Un filtro de rango (`gte`, `lte`, `gt`, `lt`) recibió un valor que no es numérico ni una fecha ISO 8601. | | [`parameter_invalid_range`](/es/errors/parameter_invalid_range) | `invalid_request_error` | 400 | Un parámetro numérico quedó fuera de sus límites. El caso habitual es `limit`, que debe estar entre 1 y 100. | | [`parameter_invalid_string`](/es/errors/parameter_invalid_string) | `invalid_request_error` | 400 | Un parámetro que debe ser texto recibió un array, un objeto o un valor que no se puede leer como cadena. | | [`parameter_invalid_url`](/es/errors/parameter_invalid_url) | `invalid_request_error` | 400 | Un campo que debe contener una URL absoluta recibió un valor que no lo es, normalmente por faltarle el esquema o el host. | | [`parameter_invalid_uuid`](/es/errors/parameter_invalid_uuid) | `invalid_request_error` | 400 | Un campo de identificador recibió un valor que no es un UUID válido. Todo `id` de recurso en v1 es un UUID. | | [`parameter_invalid_value`](/es/errors/parameter_invalid_value) | `invalid_request_error` | 422 | El valor es sintácticamente correcto pero no admisible para este recurso: fuera del catálogo canónico del campo, o incoherente con el resto del payload. | | [`parameter_missing`](/es/errors/parameter_missing) | `invalid_request_error` | 400 | El endpoint exige un parámetro que la petición no llevaba. `error.param` dice cuál. | | [`parameter_unknown`](/es/errors/parameter_unknown) | `invalid_request_error` | 400 | La petición lleva un parámetro que el endpoint no acepta: un filtro fuera de su allowlist, un campo de `sort` no ordenable, o el `page` de paginación por offset — v1 pagina por cursor. | | [`payload_too_large`](/es/errors/payload_too_large) | `invalid_request_error` | 413 | El body de la petición supera el tamaño admitido: 1 MB con carácter general, 6 MB en los endpoints que aceptan ficheros. | | [`profile_not_found`](/es/errors/profile_not_found) | `not_found_error` | 404 | La cabecera `X-Active-Profile` nombra una empresa que no existe o que no pertenece al árbol de gestoría de la clave autenticada. Ambos casos responden igual para que la API nunca revele empresas de otros tenants. | | [`resource_already_exists`](/es/errors/resource_already_exists) | `conflict_error` | 409 | Crear el objeto duplicaría uno que ya existe bajo una clave única — NIF, SKU, external id. `error.details.existing_resource_id` apunta al objeto que ya ocupa ese valor. | | [`resource_conflict`](/es/errors/resource_conflict) | `conflict_error` | 409 | La operación chocó con el estado actual del recurso y no aplica ningún código de conflicto más específico. | | [`resource_immutable`](/es/errors/resource_immutable) | `invalid_request_error` | 422 | El objeto está cerrado a cambios para esta operación: su estado o su registro contable impiden modificarlo. | | [`resource_locked`](/es/errors/resource_locked) | `conflict_error` | 409 | Otra operación retiene el recurso hasta terminar: las escrituras concurrentes sobre el mismo objeto se serializan en lugar de entrelazarse. | | [`resource_not_deletable`](/es/errors/resource_not_deletable) | `invalid_request_error` | 422 | El objeto existe, pero su estado o sus dependientes bloquean el borrado. En los borrados masivos este es el código por fila de cada entrada que no se pudo eliminar. | | [`resource_not_found`](/es/errors/resource_not_found) | `not_found_error` | 404 | El identificador no resuelve a nada visible para la empresa autenticada. Los objetos de otra empresa responden exactamente igual, a propósito. | | [`route_not_found`](/es/errors/route_not_found) | `not_found_error` | 404 | La ruta no corresponde a ningún endpoint de v1. Suele ser una errata, un prefijo `/v1` ausente o una ruta de otra área de la API. | | [`unknown_filter`](/es/errors/unknown_filter) | `invalid_request_error` | 422 | Un listado recibió un filtro que no conoce. Los parsers canónicos de v1 reportan esto como `parameter_unknown`; este código sobrevive para los endpoints aún sin migrar. | | [`unsupported_api_version`](/es/errors/unsupported_api_version) | `invalid_request_error` | 400 | La cabecera `Factuarea-Version` está bien formada pero nombra una versión fuera del conjunto soportado. | | [`unsupported_media_type`](/es/errors/unsupported_media_type) | `invalid_request_error` | 415 | Una petición con body declaró un `Content-Type` distinto de `application/json`. | ## Series [#series] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`cannot_archive_last_default_series`](/es/errors/cannot_archive_last_default_series) | `invalid_request_error` | 422 | La serie es la única activa de su tipo de documento. Archivarla dejaría a la empresa sin numeración disponible y congelaría ese tipo de documento. | | [`document_type_required_for_ambiguous_code`](/es/errors/document_type_required_for_ambiguous_code) | `invalid_request_error` | 422 | Ese código de serie existe para más de un tipo de documento, así que por sí solo no identifica una única serie. | | [`invalid_series_code`](/es/errors/invalid_series_code) | `invalid_request_error` | 422 | El código de la serie está vacío, es demasiado largo, o lleva caracteres que no corresponden a un prefijo fiscal. | | [`invalid_series_name`](/es/errors/invalid_series_name) | `invalid_request_error` | 422 | El nombre de la serie está vacío o supera la longitud permitida. | | [`invalid_series_number`](/es/errors/invalid_series_number) | `invalid_request_error` | 422 | El número inicial no es válido: no es un entero positivo, o queda en el último número ya emitido o por debajo, lo que reemitiría números ya consumidos. | | [`invalid_series_uuid`](/es/errors/invalid_series_uuid) | `invalid_request_error` | 400 | El identificador de serie de la ruta o del payload no es un UUID válido. | | [`invalid_series_year`](/es/errors/invalid_series_year) | `invalid_request_error` | 422 | El ejercicio no es un año de cuatro cifras válido para una serie de numeración. | | [`monthly_requires_month_segmented_format`](/es/errors/monthly_requires_month_segmented_format) | `invalid_request_error` | 422 | El contador se reinicia cada mes pero la máscara de numeración no segrega por mes, así que dos meses arrancarían en el mismo correlativo y producirían números duplicados dentro del año. | | [`series_already_archived`](/es/errors/series_already_archived) | `invalid_request_error` | 422 | La serie ya estaba archivada, y el archivado no se repite: una segunda llamada indica que el cliente ha perdido el estado real. | | [`series_code_immutable_with_documents`](/es/errors/series_code_immutable_with_documents) | `invalid_request_error` | 422 | Cambiar el prefijo de una serie que ya emitió documentos reescribiría retroactivamente su identificador fiscal, mientras los clientes y la AEAT tienen el número original. | | [`series_has_documents`](/es/errors/series_has_documents) | `invalid_request_error` | 422 | La serie ya numeró documentos, así que no se puede eliminar: la secuencia correlativa tiene que seguir siendo auditable. | | [`series_immutable`](/es/errors/series_immutable) | `invalid_request_error` | 405 | Las series no son editables ni eliminables vía API: la continuidad legal de la numeración exige que su prefijo, su año y su contador se queden como están. | | [`series_initial_number_creates_gap`](/es/errors/series_initial_number_creates_gap) | `invalid_request_error` | 422 | El número inicial salta más allá del siguiente correlativo natural habiendo documentos del año en curso, y ese hueco en la secuencia no es admisible para la AEAT. | | [`series_locked_by_verifactu`](/es/errors/series_locked_by_verifactu) | `invalid_request_error` | 422 | Al menos una factura de la serie tiene un registro de facturación aceptado por la AEAT, lo que congela el prefijo, el año y la base de numeración de la serie. | | [`series_not_found`](/es/errors/series_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna serie de numeración de la empresa autenticada. | | [`series_type_invalid`](/es/errors/series_type_invalid) | `invalid_request_error` | 422 | El tipo de documento de la serie queda fuera del catálogo `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`series_year_locked`](/es/errors/series_year_locked) | `invalid_request_error` | 422 | La serie ya emitió documentos en su año vigente. Mover el año dejaría esos documentos apuntando a un ejercicio vacío mientras su base imponible está en otro. | ## Servidor [#servidor] | Code | Type | HTTP | Descripción | | ----------------------------------------------------------------- | --------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`dependency_unavailable`](/es/errors/dependency_unavailable) | `service_unavailable_error` | 503 | Un servicio externo del que depende la operación no respondió a tiempo. | | [`face_transmission_failed`](/es/errors/face_transmission_failed) | `api_error` | 502 | La plataforma FACe —el punto de entrada de las administraciones públicas— estaba inaccesible o respondió con un fallo. El problema está aguas arriba, no en tu petición. | | [`facturae_signing_failed`](/es/errors/facturae_signing_failed) | `api_error` | 500 | No se pudo producir la firma XAdES del fichero Facturae, normalmente porque el certificado de firma no es utilizable en ese momento. | | [`internal_error`](/es/errors/internal_error) | `api_error` | 500 | Algo se rompió en nuestro lado al procesar la petición. La condición no la provoca tu payload. | | [`maintenance`](/es/errors/maintenance) | `service_unavailable_error` | 503 | La plataforma está en ventana de mantenimiento y las escrituras se retienen a propósito. | | [`pdf_generation_failed`](/es/errors/pdf_generation_failed) | `service_unavailable_error` | 503 | El servicio de renderizado no pudo producir el PDF. El documento y sus datos están intactos: lo que falló es el fichero. | | [`register_sealing_failed`](/es/errors/register_sealing_failed) | `api_error` | 500 | El sellado criptográfico del registro no se completó, así que el cierre quedó sin firmar en lugar de sellado con una firma rota. | | [`send_failed`](/es/errors/send_failed) | `api_error` | 500 | El documento no se entregó por email: el proveedor de correo rechazó el mensaje o estaba inaccesible. | | [`service_unavailable`](/es/errors/service_unavailable) | `service_unavailable_error` | 503 | El servicio, o una dependencia que necesita, no puede responder temporalmente. | ## Proveedores [#proveedores] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | | [`supplier_has_documents`](/es/errors/supplier_has_documents) | `invalid_request_error` | 422 | El proveedor está referenciado por facturas de compra registradas, y borrarlo dejaría esos gastos sin la parte que los emitió. | | [`supplier_not_found`](/es/errors/supplier_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún proveedor de la empresa autenticada. | ## Informes fiscales [#informes-fiscales] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`insufficient_data_for_report`](/es/errors/insufficient_data_for_report) | `invalid_request_error` | 422 | El período no tiene datos que declarar, o a una factura del período le falta un campo obligatorio para este modelo, típicamente el NIF del cliente. | | [`invalid_period`](/es/errors/invalid_period) | `invalid_request_error` | 422 | El período no identifica una declaración: el año queda fuera del rango admitido, o falta el trimestre o está fuera del rango 1 a 4 en un modelo trimestral. | | [`report_format_invalid`](/es/errors/report_format_invalid) | `invalid_request_error` | 422 | El formato queda fuera del catálogo `txt_aeat`, `pdf`, `excel`. | | [`tax_report_not_found`](/es/errors/tax_report_not_found) | `not_found_error` | 404 | El identificador no resuelve a ninguna declaración de la empresa autenticada. | | [`tax_report_type_invalid`](/es/errors/tax_report_type_invalid) | `invalid_request_error` | 422 | El tipo de declaración queda fuera del catálogo `modelo_303`, `modelo_347`, `modelo_130`. | | [`unsupported_format`](/es/errors/unsupported_format) | `invalid_request_error` | 422 | El formato pedido no está disponible para este modelo: no toda declaración produce todas las salidas. | ## Impuestos [#impuestos] | Code | Type | HTTP | Descripción | | --------------------------------------------------------------------------------------------------- | ----------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`custom_tax_creation_disabled`](/es/errors/custom_tax_creation_disabled) | `authorization_error` | 403 | La creación de impuestos personalizados está deshabilitada para esta empresa. | | [`duplicate_tax_default_for_document_type`](/es/errors/duplicate_tax_default_for_document_type) | `invalid_request_error` | 422 | Ya hay otro impuesto del mismo tipo marcado como default para ese tipo de documento, y el par (tipo de impuesto, tipo de documento) admite un único default. | | [`indirect_tax_regime_invalid`](/es/errors/indirect_tax_regime_invalid) | `invalid_request_error` | 422 | El régimen indirecto queda fuera del catálogo `iva`, `igic`, `ipsi`. | | [`invalid_aeat_code`](/es/errors/invalid_aeat_code) | `invalid_request_error` | 422 | El código de operación AEAT queda fuera del catálogo cerrado `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` que usan VeriFactu y el SII. | | [`invalid_country_aeat_zone`](/es/errors/invalid_country_aeat_zone) | `invalid_request_error` | 422 | La zona territorial AEAT queda fuera del catálogo `peninsula`, `canarias`, `ceuta`, `melilla`. | | [`invalid_country_code`](/es/errors/invalid_country_code) | `invalid_request_error` | 422 | El código de país no tiene exactamente dos caracteres, así que no es un código ISO 3166-1 alfa-2 válido. | | [`invalid_customer_visible_label`](/es/errors/invalid_customer_visible_label) | `invalid_request_error` | 422 | La etiqueta que se muestra al cliente en el documento supera la longitud permitida. | | [`invalid_description`](/es/errors/invalid_description) | `invalid_request_error` | 422 | La descripción supera la longitud máxima permitida para el campo. | | [`invalid_document_type`](/es/errors/invalid_document_type) | `invalid_request_error` | 422 | El tipo de documento queda fuera del catálogo: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`invalid_rate_for_tax_regime`](/es/errors/invalid_rate_for_tax_regime) | `invalid_request_error` | 422 | El tipo no pertenece a la rejilla legal de su régimen: el IGIC admite 0, 3, 5, 7, 9,5, 15 y 20 %; el IPSI admite 0, 0,5, 1, 2, 4, 8 y 10 %. | | [`invalid_tax_code`](/es/errors/invalid_tax_code) | `invalid_request_error` | 422 | El código del impuesto está vacío o supera los 50 caracteres. | | [`invalid_tax_name`](/es/errors/invalid_tax_name) | `invalid_request_error` | 422 | El nombre del impuesto está vacío o supera los 255 caracteres. | | [`invalid_tax_rate`](/es/errors/invalid_tax_rate) | `invalid_request_error` | 422 | El tipo impositivo queda fuera del rango permitido para su clase: IVA 0-27 %, retención 0-47 %, recargo de equivalencia 0-10 %, otros 0-100 %. | | [`invalid_tax_type_filter`](/es/errors/invalid_tax_type_filter) | `invalid_request_error` | 422 | El filtro `type` del listado por tipo lleva un valor fuera del enum `vat`, `retention`, `surcharge`, `other`. | | [`invalid_validity_window`](/es/errors/invalid_validity_window) | `invalid_request_error` | 422 | La ventana de vigencia está invertida: `valid_until` es anterior a `valid_from`. | | [`system_tax_default_modification_forbidden`](/es/errors/system_tax_default_modification_forbidden) | `authorization_error` | 403 | Los defaults de los impuestos del catálogo compartido no se fijan sobre el impuesto: el catálogo es global y la preferencia es de tu empresa. | | [`system_tax_immutable`](/es/errors/system_tax_immutable) | `invalid_request_error` | 422 | El impuesto pertenece al catálogo canónico AEAT que trae el producto. Su tipo, su código y su nombre son fijos para que todas las empresas compartan la misma referencia fiscal. | | [`system_tax_immutable_field`](/es/errors/system_tax_immutable_field) | `invalid_request_error` | 422 | La actualización toca un campo congelado en un impuesto del sistema; `error.param` dice cuál. | | [`system_tax_undeletable`](/es/errors/system_tax_undeletable) | `invalid_request_error` | 422 | Los impuestos del sistema forman parte del catálogo fiscal compartido y no se eliminan: borrarlos rompería los documentos que los referencian. | | [`tax_applies_to_invalid`](/es/errors/tax_applies_to_invalid) | `invalid_request_error` | 422 | El ámbito del impuesto queda fuera del catálogo `sale`, `purchase`, `both`. | | [`tax_code_already_exists`](/es/errors/tax_code_already_exists) | `conflict_error` | 409 | Otro impuesto del catálogo ya usa ese código, y el código identifica al impuesto sin ambigüedad. | | [`tax_id_required`](/es/errors/tax_id_required) | `invalid_request_error` | 422 | La operación necesita el número de identificación fiscal (NIF, CIF o NIE) de la parte implicada y el registro no lo tiene. | | [`tax_in_use`](/es/errors/tax_in_use) | `invalid_request_error` | 422 | El impuesto está referenciado por documentos, productos o proveedores. Eliminarlo dejaría documentos históricos sin su referencia fiscal. | | [`tax_inactive_cannot_be_default`](/es/errors/tax_inactive_cannot_be_default) | `invalid_request_error` | 422 | Un impuesto desactivado no puede quedar como default, ni global ni por tipo de documento: sería un default oculto que ningún formulario puede elegir. | | [`tax_not_found`](/es/errors/tax_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún impuesto del catálogo accesible para esta empresa. | | [`tax_type_invalid`](/es/errors/tax_type_invalid) | `invalid_request_error` | 422 | El tipo de impuesto queda fuera del catálogo `vat`, `retention`, `surcharge`, `other`. | ## VeriFactu [#verifactu] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`alta_record_not_found`](/es/errors/alta_record_not_found) | `not_found_error` | 404 | La factura no tiene registro de alta, así que la operación que depende de él no tiene sobre qué trabajar. | | [`anulacion_record_already_exists`](/es/errors/anulacion_record_already_exists) | `conflict_error` | 409 | La factura ya tiene un registro de anulación en la cadena, y la anulación se declara una sola vez. | | [`certificate_expired`](/es/errors/certificate_expired) | `invalid_request_error` | 422 | El certificado está fuera de su ventana de validez: ha caducado, o todavía no es válido. | | [`certificate_nif_mismatch`](/es/errors/certificate_nif_mismatch) | `invalid_request_error` | 422 | El NIF del titular del certificado no coincide con el de la empresa. Los registros AEAT se firman en nombre de la empresa, así que ambos deben ser el mismo. | | [`certificate_not_found`](/es/errors/certificate_not_found) | `not_found_error` | 404 | La empresa no tiene ningún certificado FNMT que corresponda al identificador, o no tiene ninguno subido. | | [`certificate_too_large`](/es/errors/certificate_too_large) | `invalid_request_error` | 422 | El fichero supera el límite de 100 KB, cuando un certificado FNMT real pesa unos pocos kilobytes. | | [`clock_drift_exceeded`](/es/errors/clock_drift_exceeded) | `invalid_request_error` | 422 | El reloj del servidor se desvió del NTP por encima del margen permitido. La marca de tiempo de generación entra en la huella AEAT, así que un reloj desincronizado produciría registros que la AEAT rechaza. | | [`declaracion_already_exists`](/es/errors/declaracion_already_exists) | `conflict_error` | 409 | La empresa ya tiene presentada la declaración responsable del SIF de ese período. | | [`declaracion_not_found`](/es/errors/declaracion_not_found) | `not_found_error` | 404 | La empresa no tiene presentada la declaración responsable del SIF del período solicitado. | | [`event_already_processed`](/es/errors/event_already_processed) | `invalid_request_error` | 422 | Ese evento del SIF ya está registrado en la cadena de eventos, y cada evento se procesa exactamente una vez. | | [`invalid_certificate_format`](/es/errors/invalid_certificate_format) | `invalid_request_error` | 422 | El fichero no es un contenedor PKCS#12: sus primeros bytes no corresponden a la estructura ASN.1 que exige el formato, diga lo que diga la extensión. | | [`invalid_certificate_password`](/es/errors/invalid_certificate_password) | `invalid_request_error` | 422 | La contraseña no abre el fichero del certificado. | | [`max_retries_exceeded`](/es/errors/max_retries_exceeded) | `invalid_request_error` | 422 | El registro agotó el presupuesto de reintentos técnicos de reenvío del XML almacenado. Reintentar el mismo contenido volvería a fallar igual. | | [`mode_switch_blocked_until_year_end`](/es/errors/mode_switch_blocked_until_year_end) | `invalid_request_error` | 422 | El modo VeriFactu se activó en este ejercicio y ya se emitió al menos un registro de facturación. Dar marcha atrás degradaría la integridad de una cadena ya declarada a la AEAT. | | [`record_already_accepted`](/es/errors/record_already_accepted) | `invalid_request_error` | 422 | La AEAT ya aceptó el registro. La aceptación es terminal y su contenido queda congelado como parte de la cadena de huellas. | | [`record_immutable`](/es/errors/record_immutable) | `invalid_request_error` | 422 | El registro pertenece a un ledger de solo-adición: una vez escrito, su contenido fiscal queda cerrado a modificaciones y a borrado. | | [`record_not_rejected`](/es/errors/record_not_rejected) | `invalid_request_error` | 422 | La subsanación solo aplica a registros que la AEAT rechazó por datos. Este registro está en otro estado — un fallo técnico, por ejemplo, lo cubre el reintento automático. | | [`record_not_subsanable`](/es/errors/record_not_subsanable) | `invalid_request_error` | 422 | El registro no se puede subsanar: no es un registro de alta, o no tiene factura de origen desde la que regenerar su contenido. | | [`requires_annulment`](/es/errors/requires_annulment) | `invalid_request_error` | 422 | El contenido regenerado cambia un campo que entra en la huella —NIF del emisor, serie y número, fecha de expedición, tipo de factura, cuota o importe total— y la cadena no se puede reescribir. | | [`sii_excluded`](/es/errors/sii_excluded) | `invalid_request_error` | 422 | La empresa está registrada en el SII, y los obligados al SII quedan excluidos del reglamento VeriFactu. | | [`verifactu_already_submitted`](/es/errors/verifactu_already_submitted) | `invalid_request_error` | 422 | La factura ya tiene su registro de alta. Existe exactamente un alta por factura, así que una segunda rompería la idempotencia de la cadena. | | [`verifactu_mode_invalid`](/es/errors/verifactu_mode_invalid) | `invalid_request_error` | 422 | El modo queda fuera del catálogo `verifactu` / `no_verifactu`. | | [`verifactu_not_eligible`](/es/errors/verifactu_not_eligible) | `invalid_request_error` | 422 | La factura no se puede registrar ahora mismo en la AEAT: la empresa no está en modo VeriFactu, no tiene certificado activo, o el certificado está revocado o emitido para otro NIF. | | [`verifactu_record_not_found`](/es/errors/verifactu_record_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún registro de facturación de la empresa autenticada. | | [`verifactu_transmission_failed`](/es/errors/verifactu_transmission_failed) | `invalid_request_error` | 422 | El envío del registro a la AEAT no llegó a completarse: el endpoint estaba inaccesible o respondió con una incidencia. | ## Webhooks [#webhooks] | Code | Type | HTTP | Descripción | | ------------------------------------------------------------------------------- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_required`](/es/errors/addon_required) | `payment_required_error` | 402 | Crear endpoints de webhook pertenece al add-on Developer API, y la empresa no lo tiene activo: el nivel gratuito permite cero endpoints. | | [`api_version_invalid_format`](/es/errors/api_version_invalid_format) | `invalid_request_error` | 422 | La versión de payload del endpoint no es una fecha `YYYY-MM-DD`. | | [`api_version_unsupported`](/es/errors/api_version_unsupported) | `invalid_request_error` | 422 | La versión de payload está bien formada pero no está entre las que sirve la plataforma. | | [`custom_header_blocklisted`](/es/errors/custom_header_blocklisted) | `invalid_request_error` | 422 | Una de las cabeceras personalizadas está reservada: la gestiona la capa HTTP (`host`, `content-type`, `content-length`, `user-agent`), la envía Factuarea como parte del contrato firmado (`factuarea-*`), o pertenece al proxy (`x-forwarded-*`). | | [`custom_header_value_too_long`](/es/errors/custom_header_value_too_long) | `invalid_request_error` | 422 | El valor de una cabecera personalizada supera los 1024 caracteres. | | [`replay_delivery_not_retryable`](/es/errors/replay_delivery_not_retryable) | `invalid_request_error` | 422 | Solo se reenvían las entregas fallidas. Una entrega que llegó bien, o una todavía en curso, no tiene nada que reenviar. | | [`replay_event_expired`](/es/errors/replay_event_expired) | `invalid_request_error` | 422 | El evento que respalda la entrega fue purgado por la política de retención de 30 días, así que ya no queda payload que reenviar. | | [`timeout_seconds_out_of_range`](/es/errors/timeout_seconds_out_of_range) | `invalid_request_error` | 422 | `timeout_seconds` queda fuera del rango de 1 a 30 segundos. | | [`too_many_custom_headers`](/es/errors/too_many_custom_headers) | `invalid_request_error` | 422 | El endpoint declara más de 20 cabeceras personalizadas. | | [`webhook_delivery_not_found`](/es/errors/webhook_delivery_not_found) | `not_found_error` | 404 | El identificador no corresponde a ningún intento de entrega, o la entrega queda fuera de la ventana de retención del histórico. | | [`webhook_endpoint_degraded`](/es/errors/webhook_endpoint_degraded) | `invalid_request_error` | 422 | El endpoint está degradado tras fallos repetidos de entrega, así que los pings de prueba se rechazan mientras siga en ese estado. | | [`webhook_endpoint_not_found`](/es/errors/webhook_endpoint_not_found) | `not_found_error` | 404 | El identificador no resuelve a ningún endpoint de webhook de la empresa autenticada. | | [`webhook_secret_recently_rotated`](/es/errors/webhook_secret_recently_rotated) | `rate_limit_error` | 429 | El secreto de firma se rotó hace menos de cinco minutos. La ventana de gracia permite que tu receptor acepte ambos secretos durante el cambio; rotar otra vez dentro de ella invalidaría firmas todavía en vuelo. | --- # Eventos (/es/guides/events) Cada evento publicado en Factuarea se **persiste** como un objeto `event` de solo lectura con un `id` opaco (un UUID v7, p. ej. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d`), coherente con el `id` de cualquier otro recurso v1. Esto te permite: * Consultarlo vía API: `GET /v1/events/{id}` y `GET /v1/events?type=invoice.paid`. * Entregarlo a los webhook endpoints suscritos (el mismo objeto se envía en el body de la entrega — ver [Webhooks](/guides/webhooks)). * Reenviar una entrega desde el dashboard (`Developers > Webhooks > Deliveries`). ## Forma del payload [#forma-del-payload] Cada evento comparte esta estructura: ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03", "api_version": "2026-05-22", "livemode": true, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } }, "created": "2026-05-15T10:23:18Z" } ``` Campos: * `id` — identificador opaco del evento (UUID v7). Úsalo como la idempotency key en tu lado. * `object` — siempre `event`. * `type` — nombre del evento como `<category>.<action>` (p. ej. `invoice.paid`, `quote.approved`). * `aggregate_id` — UUID v7 del recurso que produjo el evento (p. ej. la factura para `invoice.paid`). `null` para eventos sin agregado rellenado. Distinto de `id`, que identifica el propio evento. * `api_version` — versión por fecha bajo la que se serializó el payload, sellada en la emisión. Siempre presente en los eventos emitidos hoy; `null` solo para eventos antiguos emitidos antes de sellar las versiones. * `livemode` — `true` para eventos generados en producción (clave live, `fact_live_`); `false` para eventos generados en modo de prueba (empresa sandbox, clave `fact_test_`). Los eventos de modo de prueba se registran y se pueden consultar vía `GET /v1/events`, pero **no se entregan** a los webhook endpoints (ver [Modo de prueba y sandbox](/guides/test-mode)), así que cualquier evento que tu endpoint reciba realmente es siempre `livemode: true`. * `data` — una **referencia ligera** al recurso afectado, indexada por su tipo — p. ej. `{ "invoice": { "id": "..." } }`. Obtén el recurso desde su propio endpoint para conseguir la representación completa y actual. * `created` — timestamp ISO 8601 UTC de cuándo se creó el evento. ## Idempotencia [#idempotencia] Cada evento tiene un `id` único. Los webhooks reentregan el mismo `id` al mismo endpoint en cada reintento. En tu handler: ```python event_id = event['id'] if seen_in_db(event_id): return '', 200 process(event) mark_seen_in_db(event_id) ``` ## Catálogo de eventos [#catálogo-de-eventos] El catálogo completo y autoritativo de tipos de evento suscribibles lo devuelve `GET /v1/event-catalog`. Cada entrada lleva un `name`, una `category`, una `description` legible y un `status` (`available` o `coming_soon`): ```bash curl https://api.factuarea.com/v1/event-catalog \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ```json { "data": [ { "name": "invoice.paid", "category": "invoice", "description": "Factura pagada", "status": "available" } ], "has_more": false, "next_cursor": null } ``` Tipos de evento representativos por categoría (consulta el catálogo para la lista completa y actualizada): ### Facturas [#facturas] `invoice.created`, `invoice.auto_created`, `invoice.corrective_auto_created`, `invoice.subscription_auto_created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.cancelled`, `invoice.annulled`, `invoice.overdue`, `invoice.deleted`, `invoice.number_assigned`, `invoice.rectified`, `invoice.email_sent`, `invoice.email_failed`, `invoice.payment_reminder_sent`, `invoice.simplified_created`, `invoice.simplified_substituted`, `invoice.substituted_by_complete`, `invoice.verifactu_submitted`, `invoice.verifactu_failed`, `invoice.metadata_changed`. `invoice.auto_created` / `invoice.corrective_auto_created` / `invoice.subscription_auto_created` los emiten los flujos de [auto-facturación de pasarelas de pago](/payments/stripe-autoinvoicing) cuando un cobro, una devolución o un ciclo de suscripción generan una factura de forma automática. ### Presupuestos [#presupuestos] `quote.created`, `quote.updated`, `quote.deleted`, `quote.approved`, `quote.rejected`, `quote.converted`, `quote.expired`, `quote.marked_as_pending`, `quote.cancelled`, `quote.number_assigned`, `quote.metadata_changed`, `quote.email_sent`, `quote.email_failed`. ### Facturas proforma [#facturas-proforma] `proforma.created`, `proforma.updated`, `proforma.deleted`, `proforma.accepted`, `proforma.rejected`, `proforma.cancelled`, `proforma.expired`, `proforma.converted_to_invoice`, `proforma.number_assigned`, `proforma.metadata_changed`, `proforma.email_sent`, `proforma.email_failed`. ### Albaranes [#albaranes] `delivery_note.created`, `delivery_note.updated`, `delivery_note.status_changed`, `delivery_note.signed`, `delivery_note.converted`, `delivery_note.email_sent`, `delivery_note.email_failed`. ### Facturas de compra [#facturas-de-compra] `purchase_invoice.created`, `purchase_invoice.updated`, `purchase_invoice.paid`, `purchase_invoice.payment_registered`, `purchase_invoice.cancelled`, `purchase_invoice.metadata_changed`. ### Facturas recurrentes [#facturas-recurrentes] `recurring_invoice.created`, `recurring_invoice.activated`, `recurring_invoice.paused`, `recurring_invoice.updated`, `recurring_invoice.deleted`, `recurring_invoice.completed`, `recurring_invoice.executed`, `recurring_invoice.failed`, `recurring_invoice.cancelled`, `recurring_invoice.metadata_changed`. ### Clientes y productos [#clientes-y-productos] `client.created`, `client.updated`, `client.deleted`, `client.metadata_changed`, `product.created`, `product.updated`. ### Series e impuestos [#series-e-impuestos] `series.created`, `series.updated`, `series.deleted`, `series.archived`, `series.unarchived`, `series.marked_as_default`, `series.demoted_from_default`, `series.number_consumed`, `series.year_reset`, `series.month_reset`, `tax.metadata_changed`, `tax.validity_changed`, `tax.external_reference_changed`, `payment.received`. ### FacturaE (FACe) [#facturae-face] `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested`. ### Pagos y pasarelas [#pagos-y-pasarelas] `payout.reconciled`. `payout.reconciled` se dispara cuando un payout de Stripe se concilia con tu extracto bancario (consulta [Payouts y conciliación](/payments/payouts-reconciliation)). ## Ejemplos de payload [#ejemplos-de-payload] ### invoice.paid [#invoicepaid] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03", "api_version": "2026-05-22", "livemode": true, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } }, "created": "2026-05-15T11:42:08Z" } ``` ### client.updated [#clientupdated] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a1a", "object": "event", "type": "client.updated", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a2b", "api_version": "2026-05-22", "livemode": true, "data": { "client": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a2b" } }, "created": "2026-05-15T11:50:12Z" } ``` ### quote.converted [#quoteconverted] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a3c", "object": "event", "type": "quote.converted", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d", "api_version": "2026-05-22", "livemode": true, "data": { "quote": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d" } }, "created": "2026-05-15T12:01:55Z" } ``` El evento solo lleva una referencia ligera al recurso afectado. Obtén el recurso desde su propio endpoint (p. ej. `GET /v1/quotes/{id}`) para leer la factura convertida a la que enlaza. ## Suscribirse a eventos [#suscribirse-a-eventos] Vía API: ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.mycompany.com/factuarea/webhook", "enabled_events": ["invoice.paid", "quote.approved"] }' ``` Para suscribirte a **todos los eventos** (no recomendado en producción salvo para dashboards internos): ```json { "enabled_events": ["*"] } ``` Para suscribirte a familias enteras (todos los `invoice.*`): ```json { "enabled_events": ["invoice.*", "quote.*"] } ``` ## Listar eventos vía API [#listar-eventos-vía-api] ```bash GET /v1/events?type=invoice.paid&limit=50 ``` Filtros disponibles: `type`, `type[in]`, `created[gte]`, `created[lte]`, `created[gt]`, `created[lt]`. Paginación por cursor estándar (`limit`, `starting_after`, `ending_before`) — ver [Paginación](/guides/pagination). --- # Exportación e importación (/es/guides/export-and-import) La API pública mueve datos dentro y fuera de Factuarea con dos operaciones basadas en fichero: **exportar facturas** a una hoja de cálculo e **importar clientes** desde un CSV. Ambas reutilizan los mismos motores que el panel, y la importación sigue el contrato [partial-success](/docs/guides/bulk-operations): una fila errónea nunca hunde el fichero entero. ## Exportar facturas a una hoja de cálculo [#exportar-facturas-a-una-hoja-de-cálculo] `POST /v1/invoices/export/excel` (scope `invoices:read`) genera una hoja XLSX o CSV de tus facturas y devuelve el fichero binario. Es una operación de **lectura**: no crea ni modifica nada. Dos ejes ortogonales controlan la salida: | Parámetro | Valores | Significado | | ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `format` | `SUMMARY` (por defecto) · `ITEMS` | Layout de contenido. `SUMMARY` es **una fila por factura**; `ITEMS` es **una fila por línea de factura** (las columnas de cabecera se repiten en cada línea). | | `file_format` | `xlsx` (por defecto) · `csv` | Formato de fichero. | Elige las facturas a exportar de dos maneras: * **Por id** — pasa `invoice_ids` con los ids UUID v7 de facturas concretas. * **Por filtro** — omite `invoice_ids` y acota el conjunto con `status`, `date_from`, `date_to`, `client_id`, `series_id` y `search`. `date_from` y `date_to` filtran por fecha de emisión (ambas incluidas); `client_id` y `series_id` toman el UUID v7 público del cliente/serie. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/export/excel \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "format": "ITEMS", "file_format": "xlsx", "status": "paid", "date_from": "2026-01-01", "date_to": "2026-03-31" }' \ -o facturas-t1.xlsx ``` ### El tope de 5000 facturas [#el-tope-de-5000-facturas] El conjunto seleccionado tiene un tope de **5000 facturas**. Si tus filtros casan con más, la API **no** trunca de forma silenciosa: devuelve `422` con el código de error `export_limit_exceeded`: ```json { "error": { "type": "invalid_request_error", "code": "export_limit_exceeded", "message": "La exportación supera el máximo de 5000 facturas." } } ``` Acota el rango de fechas, el estado o el cliente, o divide la exportación en varias llamadas, para que cada petición quede por debajo del tope. <Callout type="info"> `client_id`, `series_id` y las entradas de `invoice_ids` se resuelven **dentro de tu empresa**. Un UUID inexistente o de otra empresa simplemente se descarta de la selección: nunca filtra datos entre empresas ni devuelve un `404` global. </Callout> ## Importar clientes desde un CSV [#importar-clientes-desde-un-csv] `POST /v1/clients/import` (scope `clients:write`) lee un fichero delimitado y crea un cliente por cada fila válida. La petición es **`multipart/form-data`** —lleva un fichero, no un cuerpo JSON— con tres campos: | Campo | Tipo | Significado | | --------- | -------- | ------------------------------------------------------------------------------ | | `file` | fichero | El fichero CSV/XLSX/XLS/ODS/TXT, hasta **10 MB**. | | `mapping` | objeto | `{ "cabecera_csv": "campo_destino" }`. Debe mapear al menos `name` y `tax_id`. | | `dry_run` | booleano | Si es `true`, valida y previsualiza **sin** crear nada. Por defecto `false`. | El `mapping` indica al importador qué columna de la hoja alimenta cada campo del cliente. El conjunto de destino **debe incluir `name` y `tax_id`**: sin ellos no se puede crear un cliente y la petición se rechaza con `422` antes de procesar ninguna fila. ### Descargar la plantilla [#descargar-la-plantilla] `GET /v1/clients/import/template` devuelve un CSV listo para rellenar (UTF-8 con BOM para que Excel lo abra bien) cuya fila de cabecera lista todas las columnas que entiende el importador: `Nombre`, `NIF/CIF`, `Razón social`, `Email`, `Teléfono`, campos de dirección, IVA/retención por defecto, IBAN y más. Dos filas de ejemplo muestran el formato esperado. ```bash curl -s https://api.factuarea.com/v1/clients/import/template \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -o plantilla-clientes.csv ``` ### Primero dry run, luego importar [#primero-dry-run-luego-importar] Valida siempre con `dry_run=true` antes de confirmar. La previsualización devuelve un informe por fila y **no escribe nada**: ```bash curl -s -X POST https://api.factuarea.com/v1/clients/import \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -F "file=@clientes.csv" \ -F 'mapping={"Nombre":"name","NIF/CIF":"tax_id","Email":"email"};type=application/json' \ -F "dry_run=true" ``` ```json { "data": { "object": "client_import_preview", "total_rows": 3, "rows": [ { "row": 2, "status": "valid", "errors": [], "warnings": [] }, { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] }, { "row": 4, "status": "valid", "errors": [], "warnings": [] } ] } } ``` `row` es el **número de línea (en base 1) en el fichero** (la cabecera es la fila 1, así que la primera fila de datos es la 2). `status` es `valid` o `error`; cada item de `errors[]` lleva el `param` afectado, un `code` estable y un `message` en español. Cuando la previsualización está limpia, reenvía el mismo fichero y mapeo con `dry_run=false` (u omítelo). Solo se crean las filas válidas; las rechazadas vuelven en `failures[]`, y la respuesta sigue la forma partial-success con un `results[]` por fila: ```json { "data": { "total": 3, "successful": 2, "failed": 1, "failures": [ { "index": 1, "error_code": "invalid_tax_id", "error_message": "El NIF no es válido.", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] } ], "results": [ { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] } ] } } ``` Siempre se cumple `total === successful + failed`. Una fila **duplicada** (un cliente ya existente, según la regla de deduplicación) se **omite** (`skipped`), no falla: cuenta como `successful` y no se vuelve a crear, así que reejecutar el mismo fichero es seguro. <Callout type="info"> Ramifica por `error_code` / `code`, nunca por el mensaje: el mensaje es texto en español, orientado a personas. Los códigos por fila salen del catálogo de errores v1. </Callout> ### Tope de tamaño de fichero [#tope-de-tamaño-de-fichero] La importación v1 es **síncrona** para poder devolver el resultado por fila en la misma respuesta. Los ficheros tienen un tope de **menos de 200 filas**; un fichero mayor se rechaza con `422` y el código `client_import_too_large`. Divide una lista grande en lotes por debajo del tope e impórtalos en secuencia. <Callout type="warn"> Los rechazos de `file` (10 MB) y `dry_run`, y los topes de 5000/200, se aplican antes de escribir ninguna fila. La previsualización dry-run es la forma más barata de cazar filas malformadas: úsala antes de cada importación real. </Callout> --- # Facturación FACe (B2G) (/es/guides/face-invoicing) Facturar a una administración pública española (B2G) es obligatorio a través de **FACe**, el punto general de entrada de facturas electrónicas (Ley 25/2013). Factuarea genera el XML **FacturaE 3.2.2** de cualquier factura emitida, lo firma **XAdES-EPES** con el certificado de tu empresa y lo presenta al web service de FACe — y después sigue rastreando el estado de tramitación que informa FACe hasta que la factura se paga (o se rechaza). El ciclo de vida completo lo cubren las cinco operaciones del grupo **FacturaE** de la Referencia de la API: * [Descargar el XML FacturaE](/api-reference/facturae/public-api.v1.invoices.facturae) de una factura — firmado o sin firmar, con o sin FACe. * [Enviar una factura a FACe](/api-reference/facturae/public-api.v1.invoices.face_submissions.submit). * [Listar los envíos de una factura](/api-reference/facturae/public-api.v1.invoices.face_submissions.list). * [Recuperar un envío](/api-reference/facturae/public-api.v1.face_submissions.show) para seguir su estado de tramitación. * [Solicitar la anulación](/api-reference/facturae/public-api.v1.face_submissions.cancel) de un envío. Las lecturas usan el scope `facturae:read`; enviar y anular requieren `facturae:write`. El módulo FacturaE está incluido en los planes **Empresario** y **Enterprise**. ## Antes de enviar [#prerequisites] <Steps> <Step> **Configura los tres códigos DIR3 del cliente.** Todo cliente administración pública lleva tres códigos del directorio DIR3, cada uno con el formato `^[A-Z][A-Z0-9]{8,9}$` (p. ej. `L01280796`). Configúralos al crear o actualizar el cliente: ```bash curl -X PUT https://api.factuarea.com/v1/clients/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42 \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "dir3_accounting_office": "L01280796", "dir3_managing_body": "L01280796", "dir3_processing_unit": "L01280796" }' ``` | Campo | Rol DIR3 | | ------------------------ | ----------------------- | | `dir3_accounting_office` | Oficina contable (01) | | `dir3_managing_body` | Órgano gestor (02) | | `dir3_processing_unit` | Unidad tramitadora (03) | La administración te indica los tres códigos (a menudo coinciden); también puedes consultarlos en el directorio público DIR3. </Step> <Step> **Sube un certificado de firma activo.** FACe solo acepta facturas **firmadas**, así que el envío requiere el certificado FNMT (PKCS#12) que tu empresa ya usa para VeriFactu (`POST /v1/verifactu/certificates`). Sin certificado activo el envío falla con `signing_certificate_required`. </Step> <Step> **Emite la factura.** Las facturas en borrador no pueden viajar a FACe — enviar o emitir antes la factura es lo que congela su contenido legal. Los borradores responden `invoice_not_emittable_for_facturae`. </Step> </Steps> ## Descargar el XML FacturaE [#download] Puedes descargar el XML en cualquier momento — para presentación manual, archivo o validación — sin involucrar a FACe: ```bash curl -OJ https://api.factuarea.com/v1/invoices/0197b1c2-89ab-7def-8123-456789abcdef/facturae \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Con un certificado activo el cuerpo va firmado XAdES-EPES (política de firma Facturae v3.1) y el fichero se llama `.xsig`; sin él, el XML vuelve sin firmar como `.xml`. El header de respuesta `X-Facturae-Signed: true|false` distingue ambos casos. Consulta la [referencia del endpoint](/api-reference/facturae/public-api.v1.invoices.facturae). <Callout type="info"> La descarga tolera la ausencia de certificado (obtienes el XML sin firmar); **el envío a FACe no** — FACe exige la firma. </Callout> ## Enviar a FACe [#submit] La [operación de envío](/api-reference/facturae/public-api.v1.invoices.face_submissions.submit) no lleva cuerpo de petición: la factura viaja en la ruta y los códigos DIR3 se leen del cliente en el momento del envío (y se capturan en la submission): ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197b1c2-89ab-7def-8123-456789abcdef/face-submissions \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` Respuesta (`201`): ```json { "data": { "id": "0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "object": "face_submission", "invoice_id": "0197b1c2-89ab-7def-8123-456789abcdef", "status": "submitted", "registry_number": "202612345678", "dir3_accounting_office": "L01280796", "dir3_managing_body": "L01280796", "dir3_processing_unit": "L01280796", "error_code": null, "error_message": null, "status_updated_at": "2026-06-12T10:15:00Z", "last_polled_at": null, "created_at": "2026-06-12T10:15:00Z" } } ``` `registry_number` es el asiento registral de FACe que acredita la presentación — consérvalo para cualquier disputa con la administración. ## Seguir el estado de tramitación [#states] FACe informa de cómo la administración tramita la factura. Factuarea consulta FACe periódicamente y actualiza cada envío — recuperar el [detalle del envío](/api-reference/facturae/public-api.v1.face_submissions.show) (o el [historial de envíos](/api-reference/facturae/public-api.v1.invoices.face_submissions.list) de la factura) es la forma de seguir el progreso; no hay endpoint de refresco en v1: ```bash curl https://api.factuarea.com/v1/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` | `status` | Significado | | ------------------------ | ------------------------------------------------------------------------------------------- | | `submitted` | Presentada en FACe; número de registro asignado. | | `registered_rcf` | Registrada en el RCF (el registro contable de facturas de la administración). | | `accounted` | Reconocida como obligación contable por la administración. | | `paid` | La administración informa de la factura como pagada. | | `rejected` | Rechazada por la administración — consulta el motivo en FACe y emite una factura corregida. | | `cancellation_requested` | Has solicitado la anulación; pendiente de confirmación de FACe. | | `cancelled` | Anulación confirmada por FACe. | | `error` | Error local de transmisión — `error_code` y `error_message` llevan el detalle. | ¿Prefieres push a polling? Suscríbete a los [eventos de webhook](/guides/webhooks) `facturae.face_submitted`, `facturae.face_status_changed` y `facturae.face_cancellation_requested`. ## Solicitar la anulación [#cancel] Mientras la factura no se haya pagado ni rechazado puedes [solicitar su anulación](/api-reference/facturae/public-api.v1.face_submissions.cancel) (anulación 4200). El `reason` es obligatorio y viaja a FACe: ```bash curl -X POST https://api.factuarea.com/v1/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/cancel \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "reason": "Factura emitida por error al organismo equivocado." }' ``` La anulación solo se permite en un estado anulable (`submitted`, `registered_rcf`, `accounted`); en caso contrario la llamada responde `face_submission_not_cancellable`. El envío pasa a `cancellation_requested` hasta que FACe confirma el estado final `cancelled`. ## Modo de prueba [#sandbox] Con una clave de prueba (`fact_test_`) todo el flujo se **simula**: ninguna llamada SOAP llega a FACe y el envío recibe un número de registro sintético con prefijo `FACE-SANDBOX-*`. Las validaciones de firma y DIR3 siguen aplicando, así que el sandbox ejercita los mismos caminos de error que producción. Consulta el [modo de prueba](/guides/test-mode). ## Errores [#errors] | HTTP | `code` / `subcode` | Cuándo | | ---- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 404 | `resource_not_found` | La factura o el envío no existe o pertenece a otra empresa. | | 422 | `business_rule_violation` / `invoice_not_emittable_for_facturae` | La factura está en borrador — emítela primero. | | 422 | `business_rule_violation` / `client_missing_dir3_codes` | Al cliente le falta uno o más códigos DIR3. | | 422 | `business_rule_violation` / `signing_certificate_required` | Sin certificado de firma activo — sube uno vía `POST /v1/verifactu/certificates`. | | 422 | `business_rule_violation` / `face_submission_not_cancellable` | El envío no está en un estado anulable. | | 409 | `resource_already_exists` / `face_submission_already_exists` | Ya existe un envío activo para la factura. | | 403 | `insufficient_scope` | La clave no tiene el scope `facturae:write`. | | 502 | `face_transmission_failed` | El web service de FACe está caído — no se persiste nada; reintenta más tarde. | --- # Recetario fiscal (/es/guides/fiscal-cookbook) Cada receta de abajo es una secuencia completa de llamadas, con su equivalente en el CLI `factuarea`, y un enlace a la guía que explica **por qué** se hace así. Las guías llevan el razonamiento fiscal; esta página lleva el orden de las operaciones. <Callout type="info"> El árbol de comandos del CLI se genera a partir del documento OpenAPI, así que todo endpoint es alcanzable como comando con nombre propio o mediante la vía de escape genérica `factuarea api <method> <path>`. Las recetas usan la vía de escape allí donde la forma con nombre sería adivinar; ambas llegan al mismo endpoint de la v1. Ver [Uso del CLI](/cli/usage). </Callout> Fija tu clave una sola vez: ```bash export FACTUAREA_KEY="fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ## Qué relación tiene esta página con las otras cuatro preguntas [#dimensions] Cada guía fiscal responde a cuatro preguntas sobre su escenario. Esta página es un recetario, así que las responde por delegación, y lo dice en vez de omitir las secciones. ### Cuándo aplica cada receta [#when] Se declara al principio de cada receta como su **objetivo**. Las condiciones previas —qué estado de factura admite qué operación, qué tipos de documento son elegibles— pertenecen a la guía enlazada y no se repiten aquí. ### Qué envía la API [#api] Es la única dimensión que la página cubre por completo: cada receta muestra la petición íntegra y su equivalente en el CLI, con nombres de campo reales del contrato v1. ### Qué sale en el PDF [#pdf] **No se cubre aquí.** Ninguna receta cambia el documento impreso más allá de lo que ya describe su guía — el bloque QR legal, las filas de suplidos del bloque de totales, la numeración propia de la rectificativa. Ver [Suplidos](/guides/disbursements#pdf) y [Facturas rectificativas](/guides/corrective-invoices#pdf). ### Qué llega a la AEAT [#aeat] **No se cubre aquí.** Las declaraciones que producen estas secuencias se describen en [Estados de envío VeriFactu](/guides/verifactu-submission-states#aeat) y, por escenario, en cada guía enlazada. La receta 1 es la única cuyo *propósito* es observar la declaración, y lo hace leyendo el registro de facturación. ## 1 · Emitir una factura y esperar la aceptación de la AEAT [#issue-and-wait] **Objetivo:** crear, emitir y confirmar que la Administración tributaria la dio de alta. <Steps> <Step> **Crear y emitir en una sola llamada.** `options.issue_directly` ahorra el paso de envío por separado, y los dos eventos que dispara no pueden producir un alta duplicada — el comando es idempotente por factura. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Servicio de consultoría", "quantity": 2, "unit_price": 150, "tax_rate": 21, "regime_key": "01" } ], "options": { "issue_directly": true } }' ``` ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","issued_on":"2026-06-01","due_on":"2026-07-01","lines":[{"description":"Servicio de consultoría","quantity":2,"unit_price":150,"tax_rate":21,"regime_key":"01"}],"options":{"issue_directly":true}}' ``` </Step> <Step> **Consultar el registro de facturación** hasta que salga de los estados no finales. Lee `status` y, una vez aceptado, `aeat_csv` — ese es el valor con el que concilias contra la Administración tributaria. ```bash curl https://api.factuarea.com/v1/invoices/{invoice_id}/verifactu \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` ```bash factuarea api get /v1/invoices/{invoice_id}/verifactu --json ``` </Step> <Step> **O deja de consultar.** Suscríbete en su lugar a los eventos de webhook de VeriFactu de la factura y reacciona cuando llegue el desenlace. Ver [Webhooks](/guides/webhooks). </Step> </Steps> Fundamentos: [Alta automática en VeriFactu](/guides/verifactu-auto-submission) para las compuertas que deciden si llega a crearse un registro, y [Estados de envío VeriFactu](/guides/verifactu-submission-states) para el significado de cada estado. ## 2 · Corregir un error de importe [#correct-amount] **Objetivo:** una factura emitida cobró de más. Reducirla sin anularla. Una corrección a la baja es una rectificativa **por diferencias**, con importes negativos. `correction_type: "partial"` produce esa naturaleza; una sustitución no podría llevar base negativa. ```bash curl -X POST https://api.factuarea.com/v1/invoices/{invoice_id}/corrective \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "correction_reason": "error_importe", "correction_type": "partial", "lines": [ { "description": "Ajuste por error de importe", "quantity": -1, "unit_price": 200, "tax_rate": 21 } ] }' ``` ```bash factuarea api post /v1/invoices/{invoice_id}/corrective -d '{"correction_reason":"error_importe","correction_type":"partial","lines":[{"description":"Ajuste por error de importe","quantity":-1,"unit_price":200,"tax_rate":21}]}' ``` Respuesta: `201` con la nueva factura rectificativa y una cabecera `Location`. Lista todas las rectificativas emitidas contra el original con `GET /v1/invoices/{id}/correctives`. Fundamentos: [Facturas rectificativas](/guides/corrective-invoices). Si la factura sigue sin cobrar y lo que está mal es el documento entero y no un importe, mira antes [Anular o rectificar](/guides/annul-vs-correct) — puede que la operación correcta sea la anulación. ## 3 · Sustituir facturas simplificadas por una completa [#substitute] **Objetivo:** un cliente que ha ido acumulando varios tiques necesita ahora una sola factura deducible. Una llamada. Pasas el destinatario y las facturas simplificadas que hay que agregar, y recibes una factura sustitutiva completa, ya emitida: ```bash curl -X POST https://api.factuarea.com/v1/invoices/substitute-simplified \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "simplified_invoice_ids": [ "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f601", "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f602" ], "notes": "Consumos de junio" }' ``` ```bash factuarea api post /v1/invoices/substitute-simplified -d '{"client_id":"…","simplified_invoice_ids":["…","…"],"notes":"Consumos de junio"}' ``` Los originales no se anulan: conservan su estado fiscal y dejan constancia de que han sido sustituidos. Fundamentos: [Facturas simplificadas o completas](/guides/simplified-vs-full-invoices). ## 4 · Repercutir un suplido [#disbursement] **Objetivo:** facturar tus honorarios más una tasa que pagaste por cuenta del cliente, sin que la tasa entre en tu base imponible. La línea de suplido **no lleva carga fiscal propia** y **debe** llevar la referencia de origen. Se exige al menos una línea ordinaria junto a ella. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Honorarios de constitución de sociedad", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }, { "description": "Tasa del Registro Mercantil", "quantity": 1, "unit_price": 150, "line_type": "SUPLIDO", "source_invoice_reference": "RM-2026-0451" } ] }' ``` ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","issued_on":"2026-06-01","due_on":"2026-07-01","lines":[{"description":"Honorarios","quantity":1,"unit_price":1000,"tax_rate":21},{"description":"Tasa del Registro Mercantil","quantity":1,"unit_price":150,"line_type":"SUPLIDO","source_invoice_reference":"RM-2026-0451"}]}' ``` Comprueba la respuesta: `total` vale 1210, `total_disbursements` vale 150 y `total_to_pay` vale 1360. Cobra y concilia contra `total_to_pay`, no contra `total`. Fundamentos: [Suplidos](/guides/disbursements). ## 5 · Facturar a un cliente de fuera de la UE [#export] **Objetivo:** una exportación, exenta por el art. 21 LIVA. <Steps> <Step> **Crear el cliente con una identificación alternativa.** El tipo tiene que ser legal para el país — un número de IVA intracomunitario no lo es. ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Inc", "alternative_id": { "type": "passport", "value": "X1234567", "country_code": "US" } }' ``` </Step> <Step> **Emitir con la exención declarada por línea.** El régimen de cabecera es de solo lectura en la API pública, así que la exención se declara en la línea: ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "{client_id}", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "notes": "Operación exenta por exportación (art. 21 LIVA)", "lines": [ { "description": "Suministro de equipos", "quantity": 1, "unit_price": 4000, "tax_rate": 0, "exemption_reason": "E2", "regime_key": "02" } ] }' ``` </Step> </Steps> Fundamentos: [Clientes internacionales](/guides/international-customers) — y lee su aviso sobre la inversión del sujeto pasivo antes de dar por hecho que la misma forma vale para los servicios. ## 6 · Reparar un registro que la AEAT rechazó [#repair] **Objetivo:** la Administración tributaria rechazó la declaración por un error de datos. Arreglarlo sin anular la factura. <Steps> <Step> **Confirma que es un rechazo y no un fallo técnico.** Un estado `rejected` significa que la AEAT leyó la declaración; `error` significa que nunca llegó y se reintenta de forma automática. ```bash curl "https://api.factuarea.com/v1/verifactu/records?status=rejected" \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` </Step> <Step> **Corrige el dato en su origen.** La declaración se regenera a partir de la factura y de los datos maestros *actuales* — corrige el NIF o la razón social del cliente y los valores nuevos se recogen solos. ```bash curl -X PUT https://api.factuarea.com/v1/clients/{client_id} \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{"tax_id": "B12345678"}' ``` </Step> <Step> **Reenvía.** Sin cuerpo de petición: el contenido se regenera en el servidor. ```bash curl -X POST https://api.factuarea.com/v1/verifactu/records/{record_id}/subsanar \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` ```bash factuarea api post /v1/verifactu/records/{record_id}/subsanar --json ``` </Step> <Step> **Vigila el desenlace.** El registro se transmite de nuevo y acaba aceptado — o rechazado otra vez si el dato sigue mal, en cuyo caso puedes repetir. Este camino no tiene límite de intentos. </Step> </Steps> Si la respuesta es un `422` que te dice que hace falta anular, la corrección toca un campo de la huella —el total, el número, la fecha, el NIF del emisor o el tipo de factura— y el registro no se puede reparar en el sitio. Fundamentos: [Subsanación de registros VeriFactu](/guides/verifactu-subsanacion) para la tabla completa de errores, y [Estados de envío VeriFactu](/guides/verifactu-submission-states#retry-vs-subsanar) para reintento frente a subsanación. ## Trazabilidad [#traceability] Esta página no declara ninguna regla fiscal propia: encadena llamadas cuyo fundamento está establecido en otro sitio. Cada receta **hereda** la trazabilidad de la guía que enlaza: | Receta | Hereda de | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Emitir y esperar | [Alta automática en VeriFactu](/guides/verifactu-auto-submission#traceability) · [Estados de envío VeriFactu](/guides/verifactu-submission-states#traceability) | | Corregir un importe | [Facturas rectificativas](/guides/corrective-invoices#traceability) · [Anular o rectificar](/guides/annul-vs-correct#traceability) | | Sustituir simplificadas | [Facturas simplificadas o completas](/guides/simplified-vs-full-invoices#traceability) | | Repercutir un suplido | [Suplidos](/guides/disbursements#traceability) | | Facturar fuera de la UE | [Clientes internacionales](/guides/international-customers#traceability) · [Clasificación fiscal y exenciones por línea](/guides/line-tax-classification-and-exemptions#traceability) | | Reparar un registro rechazado | [Estados de envío VeriFactu](/guides/verifactu-submission-states#traceability) | --- # Ejemplos fiscales de factura (/es/guides/fiscal-invoice-examples) La facturación española cubre muchos escenarios fiscales — B2B nacional, bienes y servicios intracomunitarios, ventas a distancia OSS, IGIC en Canarias, IPSI en Ceuta/Melilla, retención IRPF, recargo de equivalencia, operaciones exentas y no sujetas. La parte difícil es acertar la combinación correcta de `tax_rate`, `exemption_reason`, `regime_key`, `retention_rate` y `surcharge_rate` en cada línea. Para hacerlo concreto, la Referencia de la API incluye **cuatro ejemplos de request nombrados y listos para enviar** en [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) — un conjunto curado y representativo, no uno por escenario. Cada uno es un payload válido que puedes copiar, adaptar y enviar: elige el más cercano a tu caso en el desplegable de ejemplos del request body y apóyate en la tabla siguiente y en la guía enlazada en cada fila para el resto. ## Los 21 escenarios [#scenarios] Cada fila de abajo es un escenario que puedes expresar línea a línea con los campos anteriores. Los cuatro marcados con **★** son además ejemplos de request nombrados que puedes elegir directamente en el desplegable; los otros diecisiete están documentados aquí y en la guía enlazada, pero no tienen ejemplo nombrado en el spec — constrúyelos a partir del marcado más cercano. | Clave del escenario | Escenario | Guía | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ★ `b2b_nacional` | B2B nacional, IVA 21% por línea (régimen general AEAT 01). | [Claves de régimen](/guides/regime-keys) | | `b2b_nacional_iva_reducido` | IVA reducido (10%) o superreducido (4%), régimen 01. | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) | | ★ `b2c` | Consumidor final (sin NIF del destinatario; factura simplificada cuando aplique). | [Simplificadas o completas](/guides/simplified-vs-full-invoices) | | ★ `intracomunitario_bienes` | Entrega intracomunitaria de bienes, exenta `E5` (art. 25 LIVA). | [Clientes internacionales](/guides/international-customers) | | `intracomunitario_servicios` | Servicios B2B UE, inversión del sujeto pasivo — calificación `S2` (sujeta y **no** exenta, cuota repercutida `0`) derivada del régimen de cabecera `isp`, no una causa de exención. | [Clientes internacionales](/guides/international-customers) | | `oss` | Ventas a distancia OSS (IVA del país de destino), `regime_key: 17`. | [Clientes internacionales](/guides/international-customers) · [Claves de régimen](/guides/regime-keys) | | `igic_canarias` | IGIC en Canarias, `regime_key: 08`. | [Impuestos territoriales](/guides/territorial-taxes) | | `ipsi_ceuta_melilla` | IPSI en Ceuta / Melilla, `regime_key: 08`. | [Impuestos territoriales](/guides/territorial-taxes) | | ★ `con_irpf` | Retención IRPF por línea (`retention_rate`). | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) | | `con_recargo_equivalencia` | Recargo de equivalencia con un par legal IVA↔recargo, `regime_key: 18`. | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) · [Claves de régimen](/guides/regime-keys) | | `exenta_articulo_20` | Exenta por art. 20 LIVA, `exemption_reason: E1`. | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) | | `exenta_exportacion` | Exportación fuera de la UE, exenta `E2` (art. 21), `regime_key: 02`. | [Clientes internacionales](/guides/international-customers) · [Claves de régimen](/guides/regime-keys) | | `no_sujeta` | Operación no sujeta, `exemption_reason: N1` / `N2`. | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) | | `inversion_sujeto_pasivo_nacional` | Inversión del sujeto pasivo nacional (p. ej. ejecución de obra), `tax_rate: 0`. | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) | | `regimen_especial_bienes_usados` | Régimen del margen de bienes usados (REBU), `regime_key: 03`. | [Claves de régimen](/guides/regime-keys) | | `regimen_agencias_viajes` | Régimen de agencias de viajes (REAV), `regime_key: 05`. | [Claves de régimen](/guides/regime-keys) | | `criterio_caja` | Régimen del criterio de caja, `regime_key: 07`. | [Claves de régimen](/guides/regime-keys) | | `multilinea_iva_mixto` | Varias líneas a tipos de IVA distintos (21% / 10% / 4%). | [Clasificación por línea](/guides/line-tax-classification-and-exemptions) | | `con_descuento_y_metadata` | `discount_percent` por línea más `metadata` de integración. | [Recetario fiscal](/guides/fiscal-cookbook) | | `con_idempotency_key` | Reintentos seguros con el header `Idempotency-Key`. | [Recetario fiscal](/guides/fiscal-cookbook) | | `cliente_extranjero_alternative_id` | Destinatario extranjero con identificador alternativo (matriz tipo↔país AEAT). | [Clientes internacionales](/guides/international-customers) | Cada uno de los cuatro ejemplos marcados también se publica como una entrada reutilizable `components.examples.invoice_*` en el spec OpenAPI, para que los SDK y el tooling puedan resolverlos por `$ref`. <Callout type="info"> Los value objects fiscales (régimen, motivo de exención, IRPF, recargo) vienen del motor fiscal de Factuarea. Los ejemplos muestran combinaciones válidas; para el contrato campo a campo consulta el schema del request de [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) e [Importes y fechas](/guides/amounts-and-dates). </Callout> ## Facturas rectificativas por código R [#r-codes] Una factura rectificativa lleva el código de rectificación de la AEAT que indica **por qué** se corrige la original. [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective) incluye un ejemplo nombrado por cada código, cada uno un payload válido que produce ese `correction_code` exacto: | Ejemplo | Código | Aplica a | Guía | | ------------------ | ------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `r1_error_fundado` | `R1` | Error fundado de derecho / anulación. Facturas completas F1/F3. | [Facturas rectificativas](/guides/corrective-invoices) | | `r2_concurso` | `R2` | Concurso de acreedores del destinatario. F1/F3. | [Facturas rectificativas](/guides/corrective-invoices) | | `r3_incobrable` | `R3` | Créditos incobrables. F1/F3. | [Facturas rectificativas](/guides/corrective-invoices) | | `r4_otras` | `R4` | Resto de causas; total o parcial (`correction_type: partial` con `lines`). F1/F3. | [Facturas rectificativas](/guides/corrective-invoices) | | `r5_simplificada` | `R5` | Rectificación de una factura **simplificada**. Solo F2. | [Facturas rectificativas](/guides/corrective-invoices) · [Simplificadas o completas](/guides/simplified-vs-full-invoices) | Pasa `correction_code` explícitamente para seleccionar el código R; `R5` solo aplica a facturas simplificadas (F2). <Callout type="warn"> Una rectificativa es a su vez un documento fiscal: una vez emitida se reporta a la AEAT vía VeriFactu igual que cualquier otra factura. Usa el ejemplo que coincida con la causa legal — el código no es cosmético. </Callout> --- # Glosario (/es/guides/glossary) La API de Factuarea modela conceptos de facturación y cumplimiento fiscal españoles. Si integras desde fuera de España — o simplemente quieres una referencia precisa — este glosario explica los términos del dominio que aparecen en nombres de campos, valores de enum y mensajes de error, y cómo se corresponde cada uno con la API. <Callout type="info"> Los mensajes de error de la API (`error.message`) se devuelven **en español** porque reflejan la respuesta real de la API. Los campos `type`, `code` y `subcode` son identificadores estables en inglés — haz match sobre esos, no sobre el texto del mensaje. Consulta [Errors](/guides/errors). </Callout> ## Identificadores fiscales [#identificadores-fiscales] | Término | Definición | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **NIF / CIF / NIE** | El número fiscal tributario español. El *NIF* (Número de Identificación Fiscal) identifica a residentes y empresas, el *CIF* era el código heredado para personas jurídicas, y el *NIE* (Número de Identidad de Extranjero) identifica a residentes extranjeros. En la API todos residen en el único campo `tax_id` de `clients`, `suppliers` y tu cuenta. Para contrapartes no españolas usa `alternative_id` en su lugar — es mutuamente excluyente con `tax_id`. | | **VAT ID (NIF intracomunitario)** | Un número de IVA intracomunitario de la UE, expuesto como el campo `vat_id` en `clients` y `suppliers`. Distinto de `tax_id`: identifica a la parte para operaciones intracomunitarias exentas de IVA, no para fines fiscales domésticos. | | **AEAT** | Agencia Estatal de Administración Tributaria — la agencia tributaria española. Es la receptora de los registros VeriFactu, la autoridad detrás de las declaraciones [Modelo](#tax-declarations) y la emisora del [CSV](#verifactu-records--hash-chain). Todos los campos `aeat_*` y los endpoints `/v1/verifactu/aeat-access/*` se relacionan con ella. | ## Impuestos [#impuestos] | Término | Definición | | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **IVA (VAT)** | Impuesto sobre el Valor Añadido — el impuesto sobre el valor añadido español. En la API es un impuesto de `type: "vat"` en el catálogo de impuestos. Aplícalo por línea mediante `tax_rate_id`; los totales los calcula la API (`subtotal + total_vat + total_surcharge − total_retention`). Consulta la sección Taxes en la API Reference. | | **Retención (IRPF withholding)** | Una retención deducida de una línea y remitida a la AEAT en nombre del destinatario, normalmente IRPF (Impuesto sobre la Renta de las Personas Físicas) para autónomos. Se modela como un impuesto de `type: "retention"`. **Resta** del total del documento, a diferencia del IVA y el recargo. | | **Recargo de equivalencia (equivalence surcharge)** | Un régimen especial de IVA para minoristas: un recargo adicional sumado sobre el IVA para que el minorista no presente declaraciones de IVA por separado. Se modela como un impuesto de `type: "surcharge"`; una contraparte sujeta a él lleva `is_surcharge_subject: true`. **Suma** al total del documento. | ## Documentos [#documentos] | Término | Definición | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Serie (numbering series)** | La secuencia de numeración correlativa y sin huecos a la que pertenece una factura (`series_id`). Una serie es **inmutable por cumplimiento de la AEAT** — una vez creada no se puede editar (el método `PUT` devuelve `405`). El modo de prueba usa las propias series de la empresa sandbox y nunca toca tu numeración de producción. Consulta la sección Series en la API Reference y [Test mode](/guides/test-mode). | | **Rectificativa (corrective invoice)** | Una factura rectificativa que corrige una emitida previamente — la forma legal de arreglar una factura, ya que las facturas emitidas no se pueden editar ni eliminar. Se crea mediante `POST /v1/invoices/{id}/corrective`; el resultado es una factura **nueva** con `is_corrective: true` y un objeto `corrective`, mapeada a un código de tipo `R1`–`R5` de la AEAT. El código se deriva del slug `correction_reason` por defecto, pero puedes **forzarlo de forma explícita** con `correction_code` (`R1`–`R5`): una original simplificada (`F2`) solo admite `R5`, y una original completa (`F1`/`F3`) solo `R1`–`R4` — un código incompatible devuelve `422` con los códigos legales en `error.allowed_values`. Una `justification` opcional (`min:10`) registra la traza documental que la LIVA exige para algunas causas (concurso, incobrable). Compárala con **anular** (`POST /v1/invoices/{id}/annul`), que anula sin corregir. | | **Factura simplificada (simplified invoice)** | Una factura con datos reducidos (tipo `F2` de la AEAT) permitida para importes pequeños bajo el Real Decreto 1619/2012 art. 4, sin los datos completos del destinatario. Comprueba la elegibilidad con `POST /v1/invoices/simplified-eligibility`; agrupa varias en una sola factura sustitutiva completa (tipo `F3`) con `POST /v1/invoices/substitute-simplified`. Una factura ordinaria completa es de tipo `F1`. | | **Proforma** | Una factura proforma de previsualización no fiscal usada para presupuestar o solicitar el pago antes de emitir la factura real (fiscal). No lleva numeración legal y puede convertirse en factura mediante `POST /v1/proformas/{id}/convert`. Ciclo de vida: `draft`, `accepted`, `rejected`, `cancelled`, `expired`, `converted`. | | **Albarán (delivery note)** | Un documento que registra las mercancías entregadas a un cliente (el recurso `delivery_notes`), que más tarde puede convertirse en factura. Admite una firma manuscrita del destinatario (PNG en base64). Ciclo de vida público: `draft`, `sent`, `signed`, `invoiced`, `cancelled`. | | **`external_id` (clave de integración)** | Un identificador de negocio externo — el ID del registro en tu propio ERP/CRM/e-commerce — guardado en un recurso para mapearlo y deduplicarlo entre integraciones. De formato libre (≤ 100 caracteres), único por empresa y ortogonal a los identificadores propios de Factuarea (`id`, `number`, `sku`). Busca un registro por él con `POST /v1/{recurso}/find-by-external-id` (body `{ "external_id": "..." }`). Ideal como clave de mapeo al migrar desde otra plataforma — consulta [Migración desde Holded](/es/guides/migration-from-holded). | ## Cumplimiento VeriFactu y AEAT [#cumplimiento-verifactu-y-aeat] | Término | Definición | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **VeriFactu** | El sistema español de facturación antifraude (SIF) bajo el cual cada factura emitida genera un registro "Alta" a prueba de manipulaciones enviado a la AEAT. En `live` el registro se transmite a la AEAT; en `test` se crea localmente pero **nunca se transmite**. Se gestiona bajo los endpoints `/v1/verifactu/*`. Consulta [Test mode](/guides/test-mode). | | **Huella (hash chain)** | La huella encadenada SHA-256 de un registro VeriFactu (campo `huella`) que enlaza cada registro con el anterior, haciendo la secuencia a prueba de manipulaciones. Busca un registro por ella con `POST /v1/verifactu/records/find-by-huella`, y verifica la integridad de toda la cadena con `GET /v1/verifactu/chain/validate`. | | **CSV (Código Seguro de Verificación)** | El **Código Seguro de Verificación** que la AEAT devuelve cuando acepta un registro VeriFactu (el campo `aeat_csv`; `null` hasta que se asigna). Es un código de recibo de la AEAT — **no** un fichero de valores separados por comas. Busca un registro por él con `POST /v1/verifactu/records/find-by-csv`. | | **FacturaE** | El formato XML español de factura electrónica (FacturaE 3.2.2) requerido para facturación B2G a la administración pública. Descárgalo para una factura con `GET /v1/invoices/{id}/facturae` (firmado XAdES-EPES con certificado activo) y envíalo a FACe vía `/v1/face-submissions`. Consulta [Facturación FACe](/guides/face-invoicing). | | **FACe** | El punto general de entrada de facturas electrónicas de la administración pública española (Ley 25/2013). Factuarea presenta el XML FacturaE firmado al web service de FACe y sigue el estado de tramitación (`submitted` → `registered_rcf` → `accounted` → `paid`). Consulta [Facturación FACe](/guides/face-invoicing). | | **DIR3** | El directorio español de unidades de la administración pública. Todo cliente B2G lleva tres códigos DIR3 — oficina contable (01), órgano gestor (02) y unidad tramitadora (03) — requeridos por FACe, con formato `^[A-Z][A-Z0-9]{8,9}$`. | | **Declaración responsable** | Una declaración formal de cumplimiento (declaración responsable) que el productor del software SIF — Factuarea — emite para acreditar la conformidad con VeriFactu. Es a nivel de productor y de solo lectura (no por empresa): recupera la actual con `GET /v1/verifactu/declaracion-responsable`. | ## Declaraciones tributarias [#tax-declarations] | Término | Definición | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Modelo 303** | La autoliquidación trimestral española del IVA presentada ante la AEAT. Genérala con `POST /v1/tax_reports/303`, indicando el trimestre (`1`–`4`). La respuesta incluye un desglose por tipo de IVA (`{base, cuota}` en céntimos). Consulta la sección Tax reports en la API Reference. | | **Modelo 347** | La declaración informativa anual que declara a terceros con quienes las operaciones anuales superaron el umbral legal. Genérala con `POST /v1/tax_reports/347`; es anual y **no** acepta un trimestre (enviar uno devuelve un error de validación). | ## Control horario [#time-tracking] El [sistema de control horario](/guides/workforce-overview) cubre el deber español de registro de jornada. Sus términos aparecen en nombres de campo y valores de enum de los dominios de control horario, todos tras el módulo `control_horario`. | Término | Definición | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **RD-ley 8/2019** | El Real Decreto-ley 8/2019 (art. 34.9 del Estatuto de los Trabajadores), que obliga a las empresas españolas a llevar un registro diario objetivo, fiable e inalterable de la jornada de cada empleado y conservarlo cuatro años para la Inspección de Trabajo (ITSS). Factuarea lo construye como un ledger inmutable (de sola adición) sellado por una cadena de hash SHA-256 por empresa — el patrón de inviolabilidad de VeriFactu aplicado a la jornada. Consulta [Control horario](/guides/workforce-overview). | | **Fichaje (time entry)** | Cada evento de fichaje — entrada, pausa, reanudación, salida — añadido al ledger inmutable (el recurso `time_entries`) y nunca editado ni borrado. El estado de sesión en vivo (`working`, `paused`, `finished`) se deriva del ledger, no se guarda en una columna. Consulta [Fichajes](/guides/time-clock). | | **Jornada (working day)** | La jornada laboral de un empleado. Puede partirse en varios turnos (jornada partida) cuando el empleado ficha salida y vuelve a fichar entrada el mismo día; las horas semanales esperadas vienen del horario de trabajo asignado. | | **Registro inalterable (ledger)** | El registro horario inmutable y encadenado por hash. No se puede actualizar ni borrar: un error se corrige con una solicitud de corrección que añade un asiento nuevo referido al original, de modo que tanto el fallo como su arreglo quedan en el registro. Verifica su integridad con `GET /v1/time-entries/chain/validate`. | | **Cierre mensual (monthly close)** | Una instantánea que congela los saldos y el desglose de ausencias de un mes finalizado y bloquea el periodo frente a fichajes retroactivos (el recurso `monthly-register-closes`). Va de `closed ⇄ reopened`; la reapertura es una recuperación auditada. Consulta [Cierre mensual](/guides/monthly-time-close). | | **Sellado (seal)** | La firma opcional e irreversible de un cierre mensual: un digest SHA-256 canónico más una firma RSA-SHA256 desacoplada hecha con el certificado de la empresa, para que un auditor pueda probar que la instantánea no ha cambiado desde su firma. Un sellado por cierre — volver a sellar devuelve `409`. | | **Asiento de empleado (employee seat)** | La unidad de facturación del control horario. Los empleados se facturan mediante un add-on mensual dedicado (`employee-seats`) cuya cantidad sigue el censo activo; un empleado nunca cuenta contra el límite `users` del plan. Consulta [Facturación de asientos de empleado](/guides/employee-seats). | | **Tipo de ausencia (absence type)** | Lo que un empleado puede solicitar — vacaciones, baja por enfermedad, un día personal — con si es retribuida, si requiere aprobación, y una unidad de medida (`days` u `hours`). Cada empresa nueva recibe un conjunto español por defecto. Consulta [Ausencias](/guides/absences). | | **Política de ausencia (absence policy)** | La regla que decide cuánto y para quién: una dotación (`limited` días o `unlimited`), un método de devengo (`annual` o `monthly`), los tipos que cubre y los empleados a los que se asigna. | | **Saldo (balance)** | La dotación restante por empleado y tipo de ausencia, derivada del devengo de la política menos las solicitudes aprobadas (el recurso `absence-balances`). | | **Presencialidad (presence)** | La vista de solo lectura de quién está trabajando ahora mismo y quién está en oficina o en remoto hoy, derivada del ledger, los horarios y el censo — nunca persistida. No existe el scope `presence:write`: declarar presencia en oficina o remoto es una tarea solo del portal. Consulta [Presencia](/guides/presence). | --- # Idempotencia (/es/guides/idempotency) Las operaciones de escritura (`POST`, `PATCH`, `DELETE`) pueden recibirse varias veces si la conexión se corta a mitad de respuesta, tu integración reintenta tras un timeout, o hay reintentos automáticos en un gateway intermedio. Para evitar que el mismo POST cree dos facturas, la API admite el header `Idempotency-Key`. ## Cómo funciona [#cómo-funciona] 1. El cliente genera una clave única por operación (un UUID v7 es la opción recomendada, por coherencia con los identificadores de la API). 2. Envíala como header en la primera petición: ```http POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b ``` 3. La API almacena el resultado (código de estado, headers y body) asociado a esa clave durante **24 horas**. 4. Si llega una nueva petición con la misma clave dentro del TTL, la API devuelve la respuesta cacheada sin volver a ejecutar el handler. La respuesta devuelta en un replay incluye el header `Idempotent-Replayed: true` para que puedas distinguirla. ## Formato de la clave [#formato-de-la-clave] * Una **cadena opaca** para el servidor: cualquier valor único es válido (UUID v7, UUID v4, ULID, nanoid, etc.). * Longitud entre 1 y 255 caracteres. * Recomendación: UUID v7 (`Str::uuid7()`, o cualquier generador de UUID v7), por coherencia con los identificadores de la API. ```bash KEY=$(uuidgen) curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` ## Automático con los SDK oficiales [#automático-con-los-sdk-oficiales] Los [SDK de TypeScript y PHP](/sdks) adjuntan un `Idempotency-Key` a cada mutación automáticamente y **reutilizan la misma clave en los reintentos de una llamada**, de modo que una petición reintentada nunca crea por duplicado. Sobrescríbela por llamada cuando quieras deduplicación a nivel de aplicación: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts // auto-generated key await factuarea.invoices.create(body); // pin your own key (e.g. your order id) await factuarea.invoices.create(body, { idempotencyKey: "order-4711" }); ``` </Tab> <Tab value="PHP"> ```php // auto-generated key $factuarea->invoices->publicApiV1InvoicesCreate($body); // pin your own key $factuarea->invoices->publicApiV1InvoicesCreate($body, idempotencyKey: 'order-4711'); ``` </Tab> </Tabs> ## Huella del payload [#huella-del-payload] La clave queda ligada no solo al `Idempotency-Key`, sino también a una **huella** de la petición: ``` fingerprint = sha256(method + " " + path + "\n" + canonicalize(body)) ``` Donde `canonicalize(body)` es el JSON con las claves ordenadas alfabéticamente. Si reproduces la misma clave con un **payload distinto**, la API responde **409 Conflict**: ```json { "error": { "type": "idempotency_error", "code": "idempotency_key_reused", "message": "This Idempotency-Key was previously used with a different request body.", "request_id": "req_..." } } ``` Esto es una protección contra bugs: ningún caller razonable cambia el body manteniendo la misma clave. Si necesitas reintentar con datos distintos, usa una clave nueva. ## TTL [#ttl] Las entradas se persisten en la tabla `idempotency_keys` durante **86.400 segundos (24 h)**. Pasado ese tiempo, las purga un schedule diario. Si reutilizas una clave fuera de la ventana, se trata como una nueva. ## external\_id vs Idempotency-Key [#external_id-vs-idempotency-key] Ambos te protegen de duplicados, pero resuelven problemas distintos — y puedes usarlos juntos. | | `Idempotency-Key` | `external_id` | | ----------- | ------------------------------------------------------ | ----------------------------------------------------------------- | | Qué es | Un header en un único `POST`. | Una clave de negocio almacenada **en el recurso**. | | Vida | **Efímera** — ventana de 24 h, luego se purga. | **Duradera** — permanente, nunca caduca. | | Alcance | Deduplica **reintentos de transporte** de una llamada. | Deduplica por tu propia **clave de integración** (id de ERP/CRM). | | Consultable | No. | **Sí** — `POST /v1/{recurso}/find-by-external-id`. | Usa el **`Idempotency-Key`** para que un reintento sea seguro: si la red se corta a mitad de respuesta, reproducir la misma clave dentro de 24 h devuelve el resultado cacheado en lugar de crear una segunda factura. Va sobre la *entrega* de una petición. Usa **`external_id`** para vincular un recurso de Factuarea con un registro de tu propio sistema (un id de pedido, un número de documento de ERP). Envíalo en el body de creación y la API garantiza que es único por empresa (`UNIQUE(company_id, external_id)`). Más tarde puedes localizar el recurso por esa clave, sin almacenar el `id` de Factuarea: ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/find-by-external-id \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "ORDER-4711" }' | jq '.data.id' ``` En resumen: `Idempotency-Key` es una protección de reintento de corta vida; `external_id` es tu enlace permanente y consultable. Una integración típica configura **ambos** — una clave nueva por intento y un `external_id` estable por objeto de negocio. ## Recomendaciones por endpoint [#recomendaciones-por-endpoint] | Endpoint | Idempotencia recomendada | | ------------------------------------------ | -------------------------------------------------- | | `POST /v1/invoices` | **Sí** (crítica) | | `POST /v1/quotes` | **Sí** | | `POST /v1/clients` | **Sí** | | `POST /v1/invoices/{id}/send` | Sí | | `POST /v1/invoices/{id}/mark-paid` | Sí | | `POST /v1/invoices/{id}/payments` | **Sí** (un pago reintentado se contaría dos veces) | | `POST /v1/purchase_invoices/{id}/payments` | **Sí** (un pago reintentado se contaría dos veces) | | `GET /v1/...` | N/A (sin efecto) | | `PATCH /v1/...` | Opcional (PATCH es idempotente por definición) | | `DELETE /v1/...` | Opcional | Stripe documenta el mismo patrón: si vienes de allí, el contrato es idéntico. ## Ejemplo de reintento seguro [#ejemplo-de-reintento-seguro] <Tabs items="['Python (tenacity)', 'Node.js']"> <Tab value="Python (tenacity)"> ```python import os, uuid, requests from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, max=10), retry=retry_if_exception_type(requests.exceptions.RequestException), ) def create_invoice(payload): key = str(uuid.uuid4()) return requests.post( 'https://api.factuarea.com/v1/invoices', json=payload, headers={ 'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}", 'Idempotency-Key': key, }, timeout=30, ) ``` </Tab> <Tab value="Node.js"> ```javascript async function createInvoiceWithRetry(payload, maxAttempts = 5) { const key = crypto.randomUUID(); let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const res = await fetch('https://api.factuarea.com/v1/invoices', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (res.ok) return res.json(); if (res.status >= 500) { await new Promise(r => setTimeout(r, 2 ** attempt * 100)); continue; } return res.json(); } catch (err) { lastError = err; await new Promise(r => setTimeout(r, 2 ** attempt * 100)); } } throw lastError; } ``` </Tab> </Tabs> <Callout type="warn"> **Importante**: la clave debe permanecer **constante en todos los reintentos del mismo POST**. Si generas una clave nueva en cada reintento, pierdes la protección. En el ejemplo de `tenacity` en Python, la `key` se genera fuera del closure y se reutiliza en todos los reintentos. </Callout> ## Qué NO es la idempotencia [#qué-no-es-la-idempotencia] * **No** es lo mismo que el límite de peticiones: una clave idempotente reproducida dentro del TTL **no cuenta** contra tu cuota; pero claves distintas con el mismo payload sí cuentan, una a una. * **No** sustituye a un lock distribuido por tu parte. Si dos workers crean facturas concurrentemente con claves distintas, ambas se persistirán; generar la clave correctamente (p. ej. derivada de tu propio ID) es responsabilidad tuya. * **No afecta** a las respuestas `4xx` propias del servidor: si la primera petición respondió `422 invalid_request_error`, ese 422 se cachea. Reproducir la clave devuelve el mismo 422 con `Idempotent-Replayed: true`. --- # Clientes internacionales (/es/guides/international-customers) Facturar fuera de España plantea dos preguntas que el caso interior no plantea nunca: **cómo identificas a un destinatario que no tiene NIF español** y **qué recibe la AEAT por una operación exenta, con inversión del sujeto pasivo o localizada en el extranjero**. Son independientes, y esta página las responde en ese orden. ## Cuándo aplica [#when] Siempre que el destinatario no sea un obligado tributario español, o que la operación se localice fuera del territorio peninsular de aplicación del IVA. La identificación es una propiedad del **cliente**; la calificación es una propiedad de la **operación**, y un mismo cliente puede aparecer en operaciones de clases distintas. ## Identificar al cliente [#identity] Un cliente no español se identifica con `alternative_id`, un objeto `{type, value, country_code}` **mutuamente excluyente con el `tax_id` español** ([`BR-CLI-017`](#traceability)). El tipo pertenece al catálogo de identificación de la AEAT, lista L7, y cada caso tiene su propio código numérico, que viaja en la cadena VeriFactu: | `type` | Código AEAT | Significado | | ----------------------- | ----------- | ----------------------------------------------------------- | | `nif_iva` | 02 | Número de operador intracomunitario (NIF-IVA). | | `passport` | 03 | Pasaporte. | | `country_id` | 04 | Documento oficial de identificación del país de residencia. | | `residence_certificate` | 05 | Certificado de residencia fiscal. | | `other_document` | 06 | Otro documento probatorio. | | `not_registered` | 07 | No censado. | La **matriz de tipo y país** es una invariante dura, no una sugerencia: `nif_iva` solo es legal para países de la UE, porque *es* el número de operador intracomunitario; los demás tipos valen para cualquier país que no sea España; y `country_code: "ES"` se rechaza siempre, porque España usa `tax_id`. Una combinación ilegal responde `422`: ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "name": "Müller GmbH", "alternative_id": { "type": "nif_iva", "value": "DE811569869", "country_code": "DE" } }' ``` <Callout type="info"> Los valores heredados `tax_id_foreign` y `national_id` se siguen aceptando para no romper las integraciones existentes. La normalización tiene en cuenta el país: `national_id` pasa a `country_id` sin condiciones, mientras que `tax_id_foreign` pasa a `nif_iva` en un país de la UE y a `other_document` en cualquier otro caso — porque un `tax_id_foreign` de fuera de la UE no puede ser un número intracomunitario, y la matriz lo rechazaría. </Callout> Si no envías `alternative_id` en absoluto —un cliente extranjero con solo un país y un identificador fiscal—, la cadena VeriFactu cae al tipo de identificación `02`, el caso intracomunitario más frecuente. Enviar el campo de forma explícita es estrictamente mejor. ### `vat_id` es texto libre, y no se verifica [#vat-id] El campo del número de IVA intracomunitario acepta cualquier cadena de hasta 20 caracteres. **No se valida contra el registro VIES**, no se comprueba su formato por país y no se contrasta con `tax_id` ([`BR-CLI-003`](#traceability)). Un prefijo de país equivocado se acepta. Un cliente que debería estar en régimen intracomunitario pero no tiene `vat_id` no se bloquea ni se señala. `vat_id` y `tax_id` son campos separados que conviven: una empresa española puede llevar su NIF nacional y ese mismo número con el prefijo de país como número de IVA intracomunitario. ### Verificar a un destinatario español antes de facturar [#census] Para los destinatarios que **sí** tienen NIF español, [`POST /v1/clients/census-verification`](/api-reference/clients/public-api.v1.clients.verify_census) (scope `clients:read`) comprueba el par de nombre y NIF contra el censo de la AEAT antes de que factures, anticipando el rechazo VeriFactu más frecuente: el del destinatario que el censo no identifica ([`BR-CLI-015`](#traceability)). Es informativa a propósito: no bloquea nunca el guardado de un cliente ni la emisión de una factura, no persiste nada y **falla en abierto** — una AEAT inaccesible responde `200` con estado de no disponible, nunca un `5xx`. Está limitada por frecuencia, porque puede llegar a la red de la AEAT. Ver [Verificación censal](/guides/census-verification) para el flujo completo. ## El mapa de escenarios [#map] Este es el mapa del escenario de negocio a lo que recibe la AEAT ([`BR-VFC-029`](#traceability)): | Escenario | Régimen de operación de cabecera | Qué llega a la AEAT | | --------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------- | | Entrega intracomunitaria de **bienes** | `intracomunitaria` | `E5` — sujeta y exenta, art. 25 LIVA | | **Servicios** con inversión del sujeto pasivo | `isp` | `S2` — sujeta y **no** exenta, cuota repercutida `0` (la autorrepercute el destinatario) | | **Exportación** fuera de la UE | `importacion_exportacion` | `E2` — sujeta y exenta, art. 21 LIVA | | Ventas a distancia por **ventanilla única** | (general) | `regime_key: 17` — Capítulo XI del Título IX, OSS e IOSS | <Callout type="warn"> **La inversión del sujeto pasivo no es una exención.** Es una *calificación* derivada del régimen de cabecera — `S2`, sujeta y no exenta, con la cuota repercutida forzada a cero porque es el destinatario quien liquida el impuesto. **No** es una causa de exención de línea, y en particular **no** es `E4`: ese código es la exención de los arts. 23 y 24 LIVA, para depósitos aduaneros y regímenes suspensivos, que es una cosa completamente distinta. Una factura que declara la inversión del sujeto pasivo como operación exenta declara mal tanto la calificación como la cuota. </Callout> Las cuatro calificaciones alcanzables desde el régimen de cabecera son `S1` (general), `S2` (inversión del sujeto pasivo), `E5` (intracomunitaria) y `E2` (importación o exportación). Los demás códigos de exención —`E1`, `E3`, `E4`, `E6`— existen en el catálogo de la AEAT pero solo se alcanzan como causa de exención de **línea**. ## Qué envía la API [#api] Aquí viene la parte que decide cómo construyes el payload, y es una restricción real más que una preferencia de estilo. **El régimen de operación de cabecera es de solo lectura en la v1.** Ni [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) ni [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update) aceptan `operation_regime`; el objeto factura lo devuelve, y toda factura creada por la API pública nace bajo el régimen general. La causa de exención a nivel de documento es de solo lectura por el mismo motivo. El `preferred_operation_regime` del cliente —aceptado en [`POST /v1/clients`](/api-reference/clients/public-api.v1.clients.create) con los valores `general`, `intracomunitaria`, `importacion_exportacion` e `isp`— se guarda y se devuelve, pero **no** fija el régimen de las facturas que creas. Es una preferencia declarativa para tu propio uso. Lo que *sí* puedes expresar por línea es la causa de exención. Así que: | Escenario | Cómo lo expresas en la v1 | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Entrega intracomunitaria de bienes | `tax_rate: 0` + `exemption_reason: "E5"` por línea. | | Exportación fuera de la UE | `tax_rate: 0` + `exemption_reason: "E2"`, normalmente con `regime_key: "02"`. | | Ventas a distancia por ventanilla única | `regime_key: "17"` por línea, con el tipo del país de destino. | | **Inversión del sujeto pasivo** | **No expresable.** `S2` deriva del régimen de cabecera, y el catálogo de línea no contiene códigos `S` por diseño. | Esa última fila es la respuesta honesta, y tiene consecuencias: una factura con inversión del sujeto pasivo creada por la API pública quedará calificada como `S1` y con cuota repercutida, que no es lo que quieres decir. Hasta que el régimen de cabecera sea escribible, emite esas facturas desde el panel. Queda recogido en [Alcance y limitaciones](/guides/scope-and-limitations). El ejemplo publicado `intracomunitario_bienes` de la operación de creación tiene exactamente esta forma —tipo cero, más `E5`, más una clave de régimen explícita— en lugar de un régimen de cabecera que no podría fijar: ```json { "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "notes": "Entrega intracomunitaria de bienes exenta (art. 25 LIVA)", "lines": [ { "description": "Suministro de maquinaria a cliente UE (DE)", "quantity": 1, "unit_price": 5000, "tax_rate": 0, "exemption_reason": "E5", "regime_key": "01" } ] } ``` La factura simplificada no es opción en ninguno de estos escenarios: la comprobación de admisibilidad bloquea las operaciones intracomunitarias, la inversión del sujeto pasivo y cualquier destinatario fuera de España antes siquiera de mirar el importe. Ver [Facturas simplificadas o completas](/guides/simplified-vs-full-invoices#when). ## Qué sale en el PDF [#pdf] El bloque de destinatario imprime la identificación alternativa exactamente como se suministró, congelada en el momento de emitir igual que el resto del snapshot del destinatario ([`BR-INV-024`](#traceability)). La mención legal —art. 25 LIVA en una entrega intracomunitaria, art. 21 en una operación con terceros países, art. 84.Uno.2 en la inversión del sujeto pasivo— deriva del régimen de **cabecera**, y por tanto no aparece automáticamente en una factura creada por la v1 ([`BR-TAX-024`](#traceability)). Dos opciones: poner el texto en `notes`, o usar el `exemption_reason_text` de línea, que se imprime bajo la descripción de la línea y es solo de presentación. ## Qué llega a la AEAT [#aeat] **En el registro VeriFactu**, el tipo de identificación del destinatario viaja como el código AEAT de la tabla L7 de arriba, y el desglose lleva la calificación descrita en [El mapa de escenarios](#map) — códigos de operación exenta para `E5` y `E2`, y `S2` con cuota cero en la inversión del sujeto pasivo. **En la declaración anual de operaciones con terceras personas** (**Modelo 347**), las operaciones intracomunitarias y las importaciones o exportaciones quedan **excluidas** ([`BR-TXR-022`](#traceability)): se declaran por sus propias vías —la declaración recapitulativa para las operaciones intracomunitarias, y la documentación aduanera para el resto— y declararlas dos veces produciría un descuadre en la declaración cruzada. La inversión del sujeto pasivo se comporta al revés: es una operación **interior** y sí aparece en esa declaración. La clasificación usa el régimen de **cabecera** de la factura, así que una factura mixta se clasifica en bloque. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-CLI-003` — `vat_id` como texto libre, sin validación VIES, independiente de `tax_id`. * `BR-CLI-015` — verificación censal del destinatario: informativa, con fallo en abierto y sin estado. * `BR-CLI-017` — el catálogo AEAT L7 de identificación alternativa, la matriz de tipo y país, y los alias heredados que se aceptan. * `BR-INV-024` — el snapshot inmutable del destinatario. * `BR-INV-031` — el catálogo cerrado de claves de régimen usado en las líneas de ventanilla única y de exportación. * `BR-INV-032` — las causas de exención de línea y su caída al valor de la cabecera. * `BR-TAX-024` — la causa de exención a nivel de documento y su mención legal automática. * `BR-VFC-029` — el mapa de calificaciones: `S1`, `S2`, `E5` y `E2` derivadas del régimen de cabecera, y la inversión del sujeto pasivo como calificación y no como exención. * `BR-TXR-022` — exclusión de las operaciones intracomunitarias y de importación o exportación de la declaración anual de operaciones con terceros, y la inclusión de la inversión del sujeto pasivo interior. --- # Clasificación fiscal y exenciones por línea (/es/guides/line-tax-classification-and-exemptions) Una línea de factura lleva más información fiscal que un tipo impositivo. Cuatro campos opcionales deciden cómo se clasifica la operación, si se repercute IVA siquiera y cuánto paga realmente el destinatario: | Campo | Qué hace | | ------------------ | --------------------------------------------------------------------------------- | | `exemption_reason` | Declara la línea exenta (`E1`–`E6`) o no sujeta (`N1`, `N2`). | | `regime_key` | Declara el régimen especial — ver [Claves de régimen](/guides/regime-keys). | | `retention_rate` | Retención de IRPF, **restada** del importe a pagar. | | `surcharge_rate` | Recargo de equivalencia, sumado — y solo en combinaciones emparejadas legalmente. | Los cuatro son opcionales y aditivos. Una factura que los omite todos se comporta exactamente igual que antes de que existieran, huella incluida. ## Cuándo aplica [#when] Declara una causa de exención cuando la operación esté exenta o no sujeta según la Ley del IVA. Declara retención cuando factures como profesional o arriendes un local de negocio. Declara recargo cuando tu cliente sea un minorista en régimen de recargo de equivalencia. La distinción entre las dos familias de códigos es legal, no cosmética ([`BR-INV-032`](#traceability)): | Familia | Códigos | Base en la LIVA | Desglose AEAT | | ------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------- | | **Exenta** | `E1` art. 20 · `E2` art. 21 · `E3` art. 22 · `E4` arts. 23 y 24 · `E5` art. 25 · `E6` otros | La operación *sí* está sujeta al IVA, y exenta. | Declara un código de operación exenta. Sin cuota de IVA. | | **No sujeta** | `N1` arts. 7, 14 y otros · `N2` reglas de localización | La operación queda fuera del ámbito del impuesto. | Declara una calificación de no sujeción. | <Callout type="warn"> El catálogo no contiene **ningún código `S`** a propósito. «Sujeta y no exenta» es el valor por defecto, no una causa seleccionable, y **la inversión del sujeto pasivo se modela en la cabecera de la factura**, no por línea. Como el régimen de cabecera es de solo lectura en la v1, la inversión del sujeto pasivo no se puede declarar por la API pública — ver [Clientes internacionales](/guides/international-customers#map). </Callout> ## Qué envía la API [#api] ### Exención y no sujeción [#exemption] `lines[].exemption_reason` en [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) y [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). Un valor fuera del catálogo de ocho códigos responde `422` con `allowed_values`. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Exportación de maquinaria", "quantity": 1, "unit_price": 100, "tax_rate": 0, "exemption_reason": "E2" }, { "description": "Servicio de instalación", "quantity": 1, "unit_price": 50, "tax_rate": 21 } ] }' ``` El desglose de la AEAT agrupa por el par **(tipo impositivo, causa de exención)**, así que una factura mixta produce un grupo por combinación y cada grupo cuadra por su cuenta. Las líneas que comparten ambos valores se agregan en un solo grupo. Una línea que omite el campo cae a la calificación derivada de la cabecera de la factura. Como una factura creada por la v1 tiene siempre el régimen general de cabecera, ese valor por defecto es «sujeta y no exenta» — y por eso una línea exenta tiene que decirlo de forma explícita. ### La retención de IRPF resta [#irpf] `lines[].retention_rate` es un porcentaje de 0 a 100, opcionalmente acompañado de `lines[].retention_rate_id`, una referencia a una retención de tu catálogo. La fórmula canónica del total es: ``` total = subtotal + IVA − retención + recargo de equivalencia ``` La retención es dinero que el cliente se queda e ingresa en la Administración tributaria en nombre del profesional, así que **reduce** el importe a pagar ([`BR-INV-033`](#traceability)): ```json { "lines": [ { "description": "Servicios de consultoría", "quantity": 1, "unit_price": 1000, "tax_rate": 21, "retention_rate": 15 } ] } ``` Esa línea factura 1000, repercute 210 de IVA, retiene 150, y el cliente paga 1060\. Si envías **a la vez** `retention_rate` y `retention_rate_id`, tienen que coincidir. Una discrepancia es un `422` que nombra ambos porcentajes, en lugar de una decisión silenciosa sobre cuál gana. <Callout type="info"> Algunos tipos de retención se almacenan con signo negativo — una convención visual heredada que significa «esto se retiene». El cálculo toma el valor absoluto y la resta está cableada en la propia fórmula, así que el signo no cambia nunca el resultado ([`BR-TAX-008`](#traceability)). El catálogo fiscal público publica siempre estos tipos en **positivo**. </Callout> ### La matriz del recargo de equivalencia es cerrada [#surcharge] `lines[].surcharge_rate` no es un número libre. Toda línea con recargo por encima de cero se valida contra el emparejamiento legal con su tipo de IVA ([`BR-INV-034`](#traceability)): | Tipo de IVA | Recargo legal | | ----------- | ------------- | | 21 % | 5,2 % | | 10 % | 1,4 % | | 4 % | 0,5 % | | 0 % | 0 % | Una combinación ilegal —21 % de IVA con un recargo del 1,4 %, por ejemplo— responde `422` con los pares legales en `allowed_values`. La comparación es por valor redondeado a dos decimales, así que `5.2` y `5.20` son el mismo par. Las operaciones bajo este régimen suelen llevar además `regime_key: "18"`. ### Qué devuelve cada línea [#line-output] El objeto línea de factura devuelve `tax_rate`, `retention_rate`, `surcharge_rate`, `discount_percent`, el `subtotal` calculado, `taxes` y `total`, más los campos fiscales: `regime_key`, `exemption_reason`, `indirect_tax_regime` y `aeat_tax_code`. Los dos últimos son un **snapshot fiscal congelado**, escrito al construir la línea y nunca recalculado ([`BR-TAX-023`](#traceability)). Una factura emitida no cambia su régimen indirecto porque la empresa traslade después su domicilio fiscal, y las líneas históricas anteriores al snapshot se quedan vacías en lugar de rellenarse con los datos de hoy. ### De dónde salen los valores por defecto [#defaults] Cuando omites un tipo, lo resuelve una única cadena del backend compartida por todas las superficies —panel, API pública, herramientas de agente, importadores, facturas recurrentes— en orden estricto de prioridad ([`BR-TAX-025`](#traceability)): <Steps> <Step> **Valores por defecto del cliente.** El cliente guarda *tipos*, no referencias, y cada tipo se resuelve a un impuesto concreto **filtrado por el régimen indirecto del emisor**: un 7 % por defecto de un cliente en una empresa canaria resuelve a IGIC al 7 %, no a un IVA peninsular. </Step> <Step> **Ajustes de la empresa**, incluida la sugerencia derivada de la zona AEAT de la empresa. </Step> <Step> **El catálogo global.** </Step> </Steps> La cadena es de mejor esfuerzo y **nunca devuelve error** por un valor por defecto irresoluble: degrada al siguiente escalón. Si el cliente está marcado como sujeto al recargo de equivalencia y el tipo de IVA resuelto tiene un recargo legalmente vinculado, ese recargo se inyecta en los valores por defecto ([`BR-TAX-022`](#traceability)). Consúltala directamente con [`GET /v1/taxes/defaults/{docType}`](/api-reference/taxes/public-api.v1.taxes.defaults) cuando quieras enseñar a tus usuarios lo que se va a aplicar antes de que lo confirmen. ## Qué sale en el PDF [#pdf] Cambian dos cosas en el documento impreso. **El bloque de totales** refleja la fórmula de arriba: la retención aparece como resta y el recargo de equivalencia como suma, así que el importe a pagar difiere de `subtotal + IVA`. **Las menciones legales.** Cuando la factura lleva una causa de exención a nivel de documento, su frase legal —citando el artículo de la LIVA— se añade como primera mención legal de la factura ([`BR-TAX-024`](#traceability)). Esa causa es un campo de **cabecera**, uno por factura, y es de **solo lectura por la API pública**: el objeto factura expone `exemption_reason` y `legal_mentions`, pero ninguna operación de la v1 los fija. Una factura creada por la v1 no imprime, por tanto, ninguna frase automática de exención; pon el texto en `notes` si el documento lo necesita. El campo de línea `exemption_reason_text` (hasta 255 caracteres) existe con el mismo propósito a nivel de línea, y es solo de presentación — no tiene efecto fiscal. ## Qué llega a la AEAT [#aeat] Una calificación por grupo de desglose. Una línea que declara un código `E` produce una entrada de **operación exenta** con ese código literal y sin cuota repercutida; una línea que declara un código `N` produce una calificación de **no sujeción**. Una línea que no declara nada hereda la calificación derivada de la cabecera ([`BR-VFC-029`](#traceability)). La clave de agrupación es el par (tipo impositivo, causa de exención), que es lo que permite a una factura mixta pasar la validación de la AEAT: cada grupo declara su propia base, su propio tipo y su propia cuota, y dentro del grupo se cumple `base × tipo = cuota`. La retención **no** aparece en el desglose VeriFactu — no es IVA. Se declara en las declaraciones de retenciones y reduce el total de la factura. El recargo de equivalencia solo se propaga a las líneas sujetas y no exentas; las líneas exentas no llevan ni IVA ni recargo. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-INV-032` — el catálogo cerrado `E1`–`E6` / `N1`–`N2`, el valor derivado de la cabecera, la agrupación por (tipo, causa) y la invariante de huella idéntica. * `BR-INV-033` — la retención de IRPF por línea en el contrato v1 y la comprobación de coherencia entre el tipo y el impuesto referenciado. * `BR-INV-034` — la matriz legal cerrada de pares de IVA y recargo. * `BR-TAX-008` — la retención almacenada con signo pero calculada en valor absoluto. * `BR-TAX-022` — el vínculo legal de un tipo de IVA con su recargo de equivalencia. * `BR-TAX-023` — el snapshot fiscal inmutable por línea. * `BR-TAX-024` — la causa de exención a nivel de documento y la mención legal automática. * `BR-TAX-025` — la cadena cliente → empresa → catálogo global de valores fiscales por defecto. * `BR-VFC-029` — cómo se deriva la calificación cuando la línea no declara causa. --- # Migración desde Holded (/es/guides/migration-from-holded) Esta guía documenta la migración desde la API de Holded (uno de los principales competidores en el sector del SaaS de facturación español) a la Public API v1 de Factuarea. Cubre el mapeo de recursos, las diferencias de nomenclatura, los endpoints equivalentes y un script de ejemplo en Python que migra una empresa completa. ## Mapeo de recursos [#mapeo-de-recursos] | Holded | Factuarea | Notas | | --------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contacts` | `clients` + `suppliers` | Holded los mezcla en `contacts` con un campo `type`. Factuarea los separa en dos endpoints distintos. | | `products` | `products` | Nomenclatura idéntica. | | `documents/invoice` | `invoices` | Endpoint dedicado. | | `documents/estimate` | `quotes` | Cambio de nombre: Holded usa "estimate", Factuarea "quote". | | `documents/proform` | `proformas` | Renombrado a "proforma" sin abreviar. | | `documents/waybill` | `delivery_notes` | Nomenclatura canónica española/legal. | | `documents/purchase` | `purchase_invoices` | | | `documents/recurring` | `recurring_invoices` | | | `taxes` | `taxes` | Mismo concepto. | | `numerations` | `series` | Holded "numeration", Factuarea "series". El `format` de Holded se mapea a `number_format`, una máscara de numeración configurable (padding + token de año + separador), p. ej. `{code}-{YYYY}-{000}`. | | `tags` | `tags` | Etiquetas de clasificación libre en un documento (slugs en minúscula, ≤ 40 caracteres, ≤ 30 por documento). | | custom fields | `custom_fields` | Metadatos de integración tipados `[{field, value}]` en un documento (≤ 50 entradas). | | `webhooks` | `webhook_endpoints` (+ anidado `deliveries`) | Factuarea separa la configuración del endpoint de la trazabilidad de entregas (`GET /v1/webhook_endpoints/{id}/deliveries`). | ## Diferencias clave [#diferencias-clave] ### 1. Autenticación [#1-autenticación] * Holded: header `key: <api_key>`. * Factuarea: `Authorization: Bearer fact_live_...` o `X-API-Key: fact_live_...`. OpenAPI estándar. ### 2. Identificadores [#2-identificadores] * Holded: IDs opacos de tipo string-numérico. * Factuarea: cada recurso tiene una key `id` cuyo valor es un **UUID v7** (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b`) — codifica un timestamp y es ordenable lexicográficamente. Las foreign keys usan `*_id` (p. ej. `client_id`). <Callout type="info"> **Guarda el ID de Holded en `external_id` — es la estrategia de migración recomendada.** Cada recurso de Factuarea acepta un `external_id` (una clave de integración externa, ≤ 100 caracteres, única por empresa, distinta del `tax_id` fiscal). Escribe el ID original de Holded en él en cada creación. Eso hace la migración **nativamente idempotente**: no necesitas mantener una tabla de mapeo `holded_id ↔ factuarea_id` — para encontrar el registro de Factuarea de un ID de Holded, llama a `POST /v1/{recurso}/find-by-external-id` con el body `{ "external_id": "<holded_id>" }`. Disponible en `clients`, `suppliers`, `products`, `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices` y `recurring_invoices`. Consulta [external\_id en el glosario](/es/guides/glossary). </Callout> ### 3. Paginación [#3-paginación] * Holded: `?starttmp=...&endtmp=...` (timestamps en la URL). * Factuarea: paginación por cursor (`starting_after`, `ending_before`) por el `id` del recurso. Consulta [Paginación](/guides/pagination). ### 4. Errores [#4-errores] * Holded: status code + array `errors` o string `error`. * Factuarea: envoltorio `{ error: { type, code, message, request_id, doc_url } }`. Consulta [Errores](/guides/errors). ### 5. Webhooks [#5-webhooks] * Holded: payload sin firmar (validación basada en IP). * Factuarea: firma HMAC SHA256 obligatoria, tolerancia de ±5min, reintentos exponenciales hasta 8 intentos. Consulta [Webhooks](/guides/webhooks). ### 6. Idempotencia [#6-idempotencia] * Holded: no soportada. * Factuarea: header `Idempotency-Key` con TTL de 24h. Consulta [Idempotencia](/guides/idempotency). ## Endpoints equivalentes (operaciones más comunes) [#endpoints-equivalentes-operaciones-más-comunes] | Operación | Holded | Factuarea | | -------------------------------- | ---------------------------------------------------- | ---------------------------------- | | Listar facturas | `GET /invoicing/v1/documents/invoice` | `GET /v1/invoices` | | Crear factura | `POST /invoicing/v1/documents/invoice` | `POST /v1/invoices` | | Marcar factura como pagada | `POST /invoicing/v1/documents/invoice/{id}/pay` | `POST /v1/invoices/{id}/mark-paid` | | Enviar factura por email | `POST /invoicing/v1/documents/invoice/{id}/send` | `POST /v1/invoices/{id}/send` | | Descargar PDF | `GET /invoicing/v1/documents/invoice/{id}/pdf` | `GET /v1/invoices/{id}/pdf` | | Listar clientes | `GET /invoicing/v1/contacts?type=client` | `GET /v1/clients` | | Crear cliente | `POST /invoicing/v1/contacts` (con `type=client`) | `POST /v1/clients` | | Convertir presupuesto en factura | `POST /invoicing/v1/documents/estimate/{id}/convert` | `POST /v1/quotes/{id}/convert` | | Crear webhook | `POST /invoicing/v1/webhooks` | `POST /v1/webhook_endpoints` | ## Diferencias de payload [#diferencias-de-payload] ### Crear factura [#crear-factura] Holded: ```json POST /invoicing/v1/documents/invoice { "contactId": "5e1c2a3b4f5d6e7f8a9b0c1d", "date": 1747314060, "items": [ { "name": "Service", "units": 1, "subtotal": 99.00, "tax": 21 } ] } ``` Factuarea: ```json POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b { "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "lines": [ { "description": "Service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] } ``` Cambios: * `contactId` → `client_id` (FK explícita; el valor es un UUID v7). * `date` (timestamp) → `issued_on` (`YYYY-MM-DD`), con `due_on` obligatorio. * `items[].subtotal` (importe) → `lines[].unit_price` (precio unitario; la API calcula los totales). * `items[].tax` (porcentaje en línea) → `lines[].tax_rate_id` (FK al catálogo de impuestos). * `series_id` obligatorio — Factuarea exige configurar la serie antes de emitir (coherencia con la AEAT). ### Webhooks: firma [#webhooks-firma] Holded no firma. Factuarea sí (HMAC SHA256). Después de migrar **debes** validar la firma en tu handler. Consulta [Webhooks](/guides/webhooks). ## Script mínimo de migración (Python) [#script-mínimo-de-migración-python] <Callout type="warn"> Este script es ilustrativo, no listo para producción. Pruébalo en staging y valida los datos migrados manualmente antes de ejecutarlo contra producción. </Callout> ```python """ Migrate contacts and products from Holded to Factuarea. Requires: pip install requests tenacity python-dotenv """ import os, time, uuid, requests from tenacity import retry, stop_after_attempt, wait_exponential HOLDED_API = 'https://api.holded.com/api/invoicing/v1' FACTUAREA_API = 'https://api.factuarea.com/v1' HOLDED_HEADERS = {'key': os.environ['HOLDED_KEY']} FACTUAREA_HEADERS = { 'Authorization': f"Bearer {os.environ['FACTUAREA_KEY']}", 'Content-Type': 'application/json', } @retry(stop=stop_after_attempt(5), wait=wait_exponential(max=10)) def fact_post(path, payload, key=None): headers = dict(FACTUAREA_HEADERS) headers['Idempotency-Key'] = key or str(uuid.uuid4()) r = requests.post(f"{FACTUAREA_API}{path}", json=payload, headers=headers, timeout=30) if r.status_code >= 500: r.raise_for_status() return r def fetch_holded_contacts(): url = f"{HOLDED_API}/contacts" while url: r = requests.get(url, headers=HOLDED_HEADERS, timeout=30) r.raise_for_status() payload = r.json() for c in payload.get('contacts', payload if isinstance(payload, list) else []): yield c url = payload.get('next') if isinstance(payload, dict) else None def migrate_clients(): migrated = 0 for h in fetch_holded_contacts(): if h.get('type') != 'client': continue payload = { 'name': h['name'], 'tax_id': h.get('code') or h.get('vatnumber'), 'email': h.get('email'), 'phone': h.get('phone'), 'address': h.get('billAddress', {}).get('address'), 'postal_code': h.get('billAddress', {}).get('postalCode'), 'city': h.get('billAddress', {}).get('city'), 'province': h.get('billAddress', {}).get('province'), 'country': h.get('billAddress', {}).get('country', 'ES'), 'external_id': h['id'], # Holded ID → external_id: the mapping key. Reconcile later via POST /v1/clients/find-by-external-id } idem_key = f"migrate-client-{h['id']}" # deterministic for retry-safety r = fact_post('/clients', {k: v for k, v in payload.items() if v is not None}, key=idem_key) if r.status_code == 201: migrated += 1 elif r.status_code == 409: # already exists (another migration run) pass else: print(f" ERROR {r.status_code} for {h['id']}: {r.text[:200]}") time.sleep(0.1) # courtesy with rate limits print(f"Clients migrated: {migrated}") if __name__ == '__main__': migrate_clients() ``` ## Importación masiva de clientes desde el export de Holded [#client-import] El script de arriba crea los clientes uno a uno con `POST /v1/clients`. Si prefieres meter directamente el **fichero de export de contactos** de Holded, usa `POST /v1/clients/import` (scope `clients:write`, `multipart/form-data`) con el preset de mapeo de abajo. Recibe un `file` (CSV, XLSX, XLS, ODS o TXT, hasta 10 MB), un objeto `mapping` y un flag `dry_run`. ### El preset de mapeo [#client-import-mapping] En `mapping`, la **clave es la cabecera de columna tal cual aparece en tu fichero** y el **valor es el campo destino**. Holded traduce las cabeceras del export al idioma de la cuenta, así que abre la primera línea de tu fichero y ajusta las claves — los valores de la derecha no cambian nunca: ```json { "Name": "name", "Trade name": "commercial_name", "VAT number": "tax_id", "EU VAT number": "vat_id", "Email": "email", "Phone": "phone", "Mobile": "mobile", "Fax": "fax", "Website": "website", "Address": "address", "City": "city", "Postal code": "postal_code", "Province": "province", "Country": "country", "IBAN": "bank_iban", "Contact person": "contact_person", "Notes": "notes" } ``` Tres reglas que la API impone sobre el propio mapeo: * **`name` y `tax_id` son destinos obligatorios.** Un mapeo sin ambos se rechaza con `422` antes de leer una sola fila. * **Ningún destino dos veces.** Dos cabeceras apuntando al mismo campo es un error, no un «gana la última» silencioso. * **Las columnas sin mapear se ignoran.** La columna `Id` de Holded es una de ellas — mira el [paso 3](#client-import-reconcile). ### Campos destino [#client-import-fields] Estos son los campos que acepta el importador de clientes. Un destino fuera de esta tabla se **ignora en silencio**: ni se escribe ni se reporta como error, exactamente igual que si la columna no se hubiera mapeado. Revisa tu `mapping` contra esta tabla antes de la ejecución real — una errata como `"e-mail"` te cuesta la columna entera en todas las filas, y la importación sigue respondiendo `200`. | Destino | Obligatorio | Se valida como | | ------------------------ | ----------- | ------------------------------------------------------------------- | | `name` | **sí** | no vacío | | `tax_id` | **sí** | NIF/CIF/NIE español | | `commercial_name` | no | texto libre | | `vat_id` | no | texto libre | | `email` | no | dirección de correo | | `phone` | no | teléfono | | `mobile` | no | teléfono | | `fax` | no | texto libre | | `website` | no | texto libre | | `address` | no | texto libre | | `address_line2` | no | texto libre | | `address_number` | no | texto libre | | `address_floor` | no | texto libre | | `address_door` | no | texto libre | | `address_staircase` | no | texto libre | | `city` | no | texto libre | | `postal_code` | no | texto libre | | `province` | no | texto libre | | `country` | no | texto libre | | `bank_iban` | no | texto libre — pasa a ser la cuenta bancaria por defecto del cliente | | `default_vat_rate` | no | numérico | | `default_retention_rate` | no | numérico | | `default_discount` | no | numérico | | `payment_method` | no | texto libre | | `payment_terms_days` | no | numérico | | `contact_person` | no | texto libre | | `notes` | no | texto libre | Los decimales admiten tanto `.` como `,` de separador. El `tax_id` se guarda en mayúsculas, así que búscalo en mayúsculas después. Los campos que el importador **no** cubre — `external_id`, `billing_emails`, `alternative_id`, `metadata`, los códigos DIR3 y el flag de recargo de equivalencia — solo se pueden fijar con `POST /v1/clients`, `POST /v1/clients/bulk-create` o `PUT /v1/clients/{id}`. ### Paso 1 — ejecución en seco [#client-import-dry-run] Empieza siempre con `dry_run: true`. No se escribe nada, no se consume cuota mensual de filas, y obtienes el veredicto fila a fila: ```bash curl -X POST https://api.factuarea.com/v1/clients/import \ -H "Authorization: Bearer fact_live_..." \ -F "file=@holded-contactos.csv" \ -F 'mapping={"Name":"name","VAT number":"tax_id","Email":"email"}' \ -F "dry_run=true" ``` ```json { "data": { "object": "client_import_preview", "total_rows": 128, "rows": [ { "row": 2, "status": "valid", "errors": [], "warnings": [] }, { "row": 3, "status": "error", "errors": [ { "param": "tax_id", "code": "INVALID_FORMAT", "message": "..." } ], "warnings": [] } ] } } ``` `row` es el número de línea de tu fichero — la cabecera es la línea 1, así que la primera fila de datos es la `2`. Corrige en el fichero de origen cada fila con `status: "error"` y repite la ejecución en seco hasta que todas salgan `valid`. <Callout type="warn"> **La ejecución en seco previsualiza solo las 50 primeras filas.** `total_rows` cuenta el fichero entero, pero `rows[]` se corta en 50 — una ejecución en seco limpia sobre un fichero de 400 filas no significa que de la 51 en adelante esté limpio. Si el export es grande, trocéalo y haz la ejecución en seco de cada trozo. </Callout> ### Paso 2 — la importación real [#client-import-run] La misma llamada con `dry_run=false` (o sin el flag). Se aplican dos límites: * **Menos de 200 filas por petición.** Un fichero con 200 filas o más se rechaza con `422 client_import_too_large` — la importación es síncrona para poder devolver el resultado por fila en la misma respuesta. Trocea el export. * **Una cuota mensual de filas por plan**: 100 filas en Emprendedor, 1.000 en Empresario, sin límite en Enterprise. Cuenta las filas realmente importadas, sumando todas las importaciones del mes natural. La importación es **best-effort por fila**: cada fila es su propia transacción, así que una fila que falla no revierte las que ya se crearon. La respuesta te dice exactamente cuáles reenviar: ```json { "data": { "total": 128, "successful": 126, "failed": 2, "failures": [ { "index": 41, "error_code": "INVALID_FORMAT", "error_message": "...", "errors": [{ "param": "tax_id", "code": "INVALID_FORMAT", "message": "..." }] } ], "results": [ { "row": 43, "status": "error", "errors": [...], "warnings": [] } ] } } ``` `index` va desde 0 sobre las filas de datos; `row` es la línea del fichero (`index + 2`). Ojo: `successful` cuenta las filas **creadas más las omitidas**, porque el importador deduplica por `tax_id` —tanto contra los clientes que ya existen en tu empresa como contra filas repetidas dentro del mismo fichero— y una fila omitida es un éxito, no un fallo. Eso es lo que hace seguro repetir una importación, pero también significa que `successful` no es el número de clientes creados. En `results[]` solo se detallan las filas que fallaron. ### Paso 3 — reconciliar con el ID de Holded [#client-import-reconcile] <Callout type="warn"> **`external_id` no es un campo destino de la importación.** El importador de fichero escribe los campos de la tabla de arriba y ninguno más, así que la columna `Id` de Holded no puede viajar por ahí. Mapear `"Id": "external_id"` **no** se rechaza: se ignora en silencio, y la importación responde `200` como si hubiera funcionado. Estampa el id en una segunda pasada, como se explica abajo. </Callout> La correspondencia que necesitas —ID de Holded ↔ NIF— ya está en el fichero de export que acabas de subir. Estampa el `external_id` después, con una llamada por cliente: 1. Resuelve el cliente por el NIF de esa fila: ```bash curl -X POST https://api.factuarea.com/v1/clients/find-by-tax-id \ -H "Authorization: Bearer fact_live_..." \ -H "Content-Type: application/json" \ -d '{ "tax_id": "B12345678" }' ``` Devuelve `200` con el cliente, o `404 client_not_found` si esa fila fue una de las que falló en el paso 2. 2. Escríbele el ID de Holded. El `PUT` de un cliente es una actualización parcial, así que enviar solo `external_id` deja intacto el resto de campos: ```bash curl -X PUT https://api.factuarea.com/v1/clients/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01 \ -H "Authorization: Bearer fact_live_..." \ -H "Content-Type: application/json" \ -d '{ "external_id": "5e1c2a3b4f5d6e7f8a9b0c1d" }' ``` A partir de ahí los clientes importados se comportan igual que los que crea el script: `POST /v1/clients/find-by-external-id` los resuelve por su ID de Holded, y la migración de facturas puede referenciarlos sin tabla de correspondencias. <Callout type="info"> **¿Quieres el `external_id` de una pasada?** Entonces no uses el importador de fichero. `POST /v1/clients/bulk-create` admite hasta 500 payloads de cliente por lote, tiene el mismo flag `dry_run` y el mismo contrato de resultado por fila, y acepta `external_id` en cada payload — lee tú el export y envíalo como JSON. Mira [Operaciones masivas](/guides/bulk-operations). </Callout> ## Checklist de migración [#checklist-de-migración] 1. **Inventario**: número de contactos, productos, facturas históricas, webhooks activos. 2. **Mapea con `external_id`** (recomendado): escribe cada ID de Holded en el `external_id` del recurso de Factuarea correspondiente al crearlo. Así no necesitas una tabla intermedia `holded_id ↔ factuarea_id` — para resolver una relación (factura → cliente) o para reejecutar la migración con seguridad, busca el registro con `POST /v1/{recurso}/find-by-external-id` (body `{ "external_id": "<holded_id>" }`). Esto es lo que hace idempotente la migración. 3. **Migración por fases**: * Catálogos: impuestos, series, productos → primero. * Maestros: clientes, proveedores → segundo. * Documentos históricos: facturas, presupuestos, etc. → tercero. 4. **Doble escritura temporal**: durante 1–2 semanas, escribe en ambas plataformas. Reconcilia las diferencias a diario. 5. **Webhooks**: configura los nuevos endpoints, despliega el handler con verificación HMAC y ejecútalo en paralelo. 6. **Cut-over**: deja de escribir en Holded, deshabilita los webhooks allí. 7. **Soporte**: contacta con `support@factuarea.com` indicando el `request_id` ante cualquier incidencia durante la migración. ## Diferencias intencionadas [#diferencias-intencionadas] Algunos comportamientos de Holded **no replicamos** a propósito: * **Anular vs eliminar una factura**: Holded permite eliminar facturas. Factuarea no — emitir y luego eliminar es un anti-patrón frente a la AEAT. Usa `POST /v1/invoices/{id}/annul` (anular) o emite una factura rectificativa. * **Editar una factura emitida**: Holded permite reemitir un PDF distinto. Factuarea bloquea los cambios después de `sent` salvo `mark-paid`, `annul`, `create-corrective`. Es deliberado. * **Calculadora de IVA en línea**: Holded acepta el porcentaje de IVA en cada línea. Factuarea requiere una FK al catálogo de impuestos para garantizar la coherencia y los informes. Son decisiones de producto, no limitaciones técnicas. Si encuentras un caso de uso real que no podamos cubrir, contacta con producto. --- # Cierre mensual del registro (/es/guides/monthly-time-close) Un **cierre mensual** congela el registro de jornada de un `(año, mes)` finalizado. Toma un **snapshot** de los totales de saldo y del desglose de ausencias de cada empleado activo —reutilizando el contrato de balances, sin recalcular— y **bloquea el periodo** contra fichajes retroactivos y correcciones. Es el paso que convierte un ledger en curso en un registro mensual defendible. Todos los endpoints viven bajo `https://api.factuarea.com/v1`; cerrar y reabrir usan `time_entries:write`, las lecturas y exportaciones usan `time_entries:read` (las exportaciones para nóminas usan `payroll_exports:read`). ## Cerrar un mes [#close] `POST /v1/monthly-register-closes` cierra un mes finalizado. `year` y `month` (1–12) son obligatorios. Un mes **que aún no ha terminado** devuelve `422`; un periodo **ya cerrado** devuelve `409`. El cierre se crea en estado `closed` y la respuesta lleva una cabecera `Location` que apunta a él. ```bash curl -X POST https://api.factuarea.com/v1/monthly-register-closes \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "year": 2026, "month": 1 }' ``` Un cierre se mueve entre dos estados, `closed ⇄ reopened`; ninguno es terminal. **Reabrir** (`POST /v1/monthly-register-closes/{close}/reopen`) es una recuperación auditada de un cierre erróneo que vuelve a permitir escrituras en el periodo. Re-cerrar un mes reabierto conserva su `id` original — el cierre reabierto y re-cerrado es el **mismo** recurso. Lista cierres con `GET /v1/monthly-register-closes` (ordenados por periodo descendente, filtrables por `year`) y obtén uno con `GET /v1/monthly-register-closes/{close}`. ## Sellarlo con una firma digital [#seal] `POST /v1/monthly-register-closes/{close}/seal` **sella** un registro `closed`: congela un digest SHA-256 canónico del snapshot y una **firma RSA-SHA256 separada** hecha con el certificado de la empresa. El registro queda a prueba de manipulación y verificable de forma independiente por un tercero. Hay **un sello por cierre** — volver a sellar devuelve `409`. Sellar un cierre que no está `closed` devuelve `422`, y una empresa sin certificado activo y usable devuelve `422`. Recupera el sello y su **estado de verificación en vivo** con `GET /v1/monthly-register-closes/{close}/seal`: `verified` es `true` cuando el snapshot y la firma están intactos; en caso contrario, `verification_reason` explica el desajuste (`snapshot_mismatch`, `signature_invalid` o `certificate_unreadable`). ```bash curl -X POST https://api.factuarea.com/v1/monthly-register-closes/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/seal \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` <Callout type="info"> El sello es opcional pero recomendable: el cierre por sí solo bloquea el periodo, y el sello añade una firma criptográfica que permite a un auditor probar que el snapshot no ha cambiado desde que se firmó. </Callout> ## Informe y exportaciones [#exports] Tres salidas de lectura se construyen desde el **snapshot congelado**, de modo que los totales nunca se desvían de la hoja del momento del cierre. | Salida | Endpoint | Qué obtienes | | ------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Informe mensual | `GET /v1/monthly-register-closes/{close}/report` | Totales agregados de la empresa más una fila por empleado (totales, desglose de ausencias, saldos) y el detalle diario. Totales en minutos. | | Exportación del registro diario | `GET /v1/monthly-register-closes/{close}/export` | El registro diario como hoja de cálculo en el formato `rdley_8_2019`, leído del ledger bloqueado. Descarga binaria. | | Incidencias para nómina | `GET /v1/monthly-register-closes/{close}/payroll-export` | Una fila por empleado (identidad fiscal, minutos trabajados vs esperados, horas extra, saldo, ausencias aprobadas por tipo) en `a3`, `sage` o `nominasol`. Descarga binaria. | El informe es un **recurso computado**: expone `close_id`, nunca un `id` propio. Para los ficheros de exportación y de nómina, `format` es opcional (por defecto `rdley_8_2019` y `a3` respectivamente); un valor fuera del catálogo devuelve `422`, y un periodo sin cierre devuelve `404`. Lista el software de nómina soportado con `GET /v1/payroll-export-formats`. ```bash curl -G https://api.factuarea.com/v1/monthly-register-closes/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/payroll-export \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "format=a3" \ --output payroll-2026-01.xlsx ``` Consulta los esquemas en la [Referencia de API](/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.create). ## Flujo típico [#flow] 1. **Cierra** el mes finalizado (`POST .../monthly-register-closes`). 2. **Séllalo** si necesitas un registro firmado y verificable (`POST .../{close}/seal`). 3. **Informa o exporta** para auditoría (`/report`), el fichero de inspección (`/export`) o nómina (`/payroll-export`). 4. Si detectas un error, **reabre**, corrige las entradas y **re-cierra** — el `id` se mantiene igual. ## Próximos pasos [#next] * [Fichajes](/guides/time-clock) — las entradas y correcciones que el cierre fotografía. * [Ausencias](/guides/absences) — las ausencias aprobadas que aparecen en el informe. --- # Paginación (/es/guides/pagination) Todos los endpoints de listado de la API pública usan **paginación por cursor**. Misma semántica que Stripe / Linear: paginas por un identificador opaco (el `id` del recurso), no por número de página. Esto garantiza resultados estables incluso cuando se crean nuevos recursos durante la iteración. ## Parámetros [#parámetros] | Parámetro | Tipo | Por defecto | Rango | Descripción | | ---------------- | ------- | ----------- | ----------------- | ----------------------------------------------------------------------------------- | | `limit` | integer | `25` | `1`–`100` | Número de elementos por página. | | `starting_after` | string | `null` | id (UUID v7) | Devuelve los elementos creados **después** del recurso cuyo `id` se pasa. | | `ending_before` | string | `null` | id (UUID v7) | Devuelve los elementos creados **antes** del recurso cuyo `id` se pasa. | | `sort` | string | `-created` | campo por recurso | Campo de orden; prefijo `-` para descendente (p.ej. `-total`). Ver [Orden](#orden). | <Callout type="warn"> `starting_after` y `ending_before` son mutuamente excluyentes. Enviar ambos en la misma petición responde `422` con un envoltorio de error `invalid_request_error`. </Callout> ## Forma de la respuesta [#forma-de-la-respuesta] ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "...": "..." }, { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "...": "..." } ], "has_more": true, "next_cursor": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c" } ``` * `data`: array de hasta `limit` elementos, ordenados por `id` DESC (equivalente a `created_at` DESC porque usamos UUID v7). * `has_more`: `true` si hay más elementos antes del primero de `data` (más nuevos) al paginar con `starting_after`, o después del último al paginar con `ending_before`. * `next_cursor`: `id` del último elemento de `data`. Pásalo como `starting_after` en la siguiente petición para avanzar. Cuando no hay más elementos, `has_more` es `false` y `next_cursor` es `null`. ## Iterar todos los resultados [#iterar-todos-los-resultados] <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests def iterate(endpoint): cursor = None while True: params = {'limit': 100} if cursor: params['starting_after'] = cursor resp = requests.get( f'https://api.factuarea.com/v1/{endpoint}', params=params, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() body = resp.json() yield from body['data'] if not body['has_more']: break cursor = body['next_cursor'] for invoice in iterate('invoices'): print(invoice['id'], invoice['number']) ``` </Tab> <Tab value="Node.js"> ```javascript async function* iterate(endpoint) { let cursor = null; while (true) { const url = new URL(`https://api.factuarea.com/v1/${endpoint}`); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('starting_after', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}` }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const body = await res.json(); for (const item of body.data) yield item; if (!body.has_more) break; cursor = body.next_cursor; } } for await (const invoice of iterate('invoices')) { console.log(invoice.id, invoice.number); } ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash cursor="" while : ; do if [ -z "$cursor" ]; then url="https://api.factuarea.com/v1/invoices?limit=100" else url="https://api.factuarea.com/v1/invoices?limit=100&starting_after=$cursor" fi resp=$(curl -s -H "Authorization: Bearer $FACTUAREA_API_KEY" "$url") echo "$resp" | jq -c '.data[]' has_more=$(echo "$resp" | jq -r '.has_more') cursor=$(echo "$resp" | jq -r '.next_cursor') [ "$has_more" = "true" ] || break done ``` </Tab> </Tabs> ## Iterar con el SDK oficial [#iterar-con-el-sdk-oficial] Los [SDK de TypeScript y PHP](/sdks) ocultan el cursor por completo: los métodos de listado devuelven un iterador que recorre todas las páginas por ti. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts const page = await factuarea.invoices.list({ status: "paid", limit: 50 }); // iterate every item across every page — cursors handled internally for await (const invoice of page) { console.log(invoice.id, invoice.number); } // or walk page by page page.data; // items on this page page.hasMore; // boolean page.nextCursor; // opaque cursor or null const next = await page.getNextPage(); // Page | null const all = await page.toArray(); // collect everything ``` </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\Pagination\PageIterator; use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest; $pages = new PageIterator( fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList( new PublicApiV1InvoicesListRequest(startingAfter: $cursor), )->rawResponse, ); foreach ($pages->items() as $invoice) { echo $invoice['id'], PHP_EOL; } ``` </Tab> </Tabs> Consulta [SDKs › Paginar con el SDK](/sdks#paginating-with-the-sdk) para ver la superficie completa. ## Orden [#orden] Pasa `?sort=<campo>` para ordenar un listado. Un campo a secas ordena de forma **ascendente**; el prefijo `-` ordena de forma **descendente** (p.ej. `?sort=-total`). Si se omite, el valor por defecto es **`-created`** — equivalente a `id` DESC, un orden total y estable porque usamos UUID v7 (que codifica una marca de tiempo en los 48 bits altos, así que "los más recientemente creados primero" no necesita ninguna columna `created_at` adicional). El cursor (`starting_after` / `ending_before`) sigue funcionando con el orden que elijas: el campo escogido es el criterio principal y el `id` del recurso es un criterio secundario estable, de modo que la paginación se mantiene determinista incluso cuando varias filas comparten el mismo valor (al estilo Stripe). Los campos `sort` permitidos están acotados **por recurso** — enviar un campo no soportado responde `422` con un envoltorio de error `invalid_request_error`: | Recurso | Campos `sort` permitidos | | -------------------- | ------------------------------------------- | | `invoices` | `created`, `total`, `number` | | `quotes` | `created`, `total`, `number`, `valid_until` | | `proformas` | `created`, `total`, `number`, `valid_until` | | `delivery_notes` | `created`, `number`, `delivery_date` | | `purchase_invoices` | `created`, `total`, `issued_on`, `due_on` | | `recurring_invoices` | `created`, `next_run_at` | ```bash # Facturas, mayor total primero curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "sort=-total" # Presupuestos por fecha de validez, los que vencen antes primero curl -G https://api.factuarea.com/v1/quotes \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "sort=valid_until" ``` ## ¿Por qué no `?page=`? [#por-qué-no-page] Las páginas numéricas tienen problemas cuando el conjunto de datos cambia durante la iteración: * Crear un recurso entre páginas → duplica filas. * Eliminar un recurso entre páginas → omite filas. * `COUNT(*)` es costoso pasadas unos pocos miles de filas. La paginación por cursor con UUID v7 elimina ambos: el cursor apunta a una posición estable en el tiempo, no a un desplazamiento variable. Por eso no hay un parámetro de desplazamiento `?page=N` — la única forma de paginar por una lista es el cursor `starting_after` / `ending_before`. Un valor de cursor inválido responde `422` con un envoltorio de error `invalid_request_error` (`code: parameter_invalid_cursor`): ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid_cursor", "message": "The provided cursor is not a valid resource id.", "param": "starting_after", "request_id": "req_..." } } ``` ## Caso de uso: obtener solo resultados nuevos [#caso-de-uso-obtener-solo-resultados-nuevos] Si tu integración hace polling cada N minutos, guarda el `next_cursor` (el `id` más reciente que has visto) entre cada sondeo. En la siguiente pasada usa `ending_before=<saved_cursor>` para obtener solo los elementos **más nuevos** que ese punto. ```python last_seen = load_last_cursor() # resource id stored in your DB resp = requests.get( 'https://api.factuarea.com/v1/invoices', params={'limit': 100, 'ending_before': last_seen} if last_seen else {'limit': 100}, headers={'Authorization': f"Bearer {API_KEY}"}, ) new_invoices = resp.json()['data'] if new_invoices: save_last_cursor(new_invoices[0]['id']) # the newest one ``` --- # Registrar pagos (/es/guides/payments) Las facturas y las facturas de compra mantienen un **ledger de pagos**: una lista de pagos individuales, cada uno con su propio importe, fecha y método. Registra los pagos de uno en uno a medida que entra el dinero — la API recalcula los importes **cobrado** y **pendiente** después de cada entrada y pasa el documento a `paid` cuando el saldo llega a cero. No existe un estado «parcialmente pagada» aparte. El avance del cobro se lee a partir de dos campos derivados, de solo presentación, en la factura: `paid_amount` (suma del ledger) y `pending_amount` (`total − paid_amount`). Un documento con `pending_amount > 0` sigue `pending`; aquel cuyo `pending_amount` llega a `0` pasa a `paid`. ## Registrar un pago de venta [#registrar-un-pago-de-venta] `POST /v1/invoices/{id}/payments` añade un pago a una factura de venta. El body es pequeño: | Campo | Tipo | Requerido | Notas | | ---------------- | --------------------- | --------- | --------------------------------------------------------- | | `amount` | number | **Sí** | Mayor que `0`. No puede superar `pending_amount`. | | `paid_on` | string (`YYYY-MM-DD`) | **Sí** | La fecha en que se recibió el dinero. | | `payment_method` | string (enum) | **Sí** | Uno de los valores del catálogo (ver abajo). | | `reference` | string | No | Tu propia referencia (p. ej. un número de transferencia). | | `notes` | string | No | Nota interna libre. | `payment_method` es un enum cerrado de siete valores: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`. Obtén el catálogo con etiquetas desde [`GET /v1/payment-methods`](#payment-methods) en lugar de fijar los valores a mano. La respuesta es `201 Created` con el pago recién creado bajo `data`: ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } } ``` <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests resp = requests.post( 'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments', json={ 'amount': 500.00, 'paid_on': '2026-05-20', 'payment_method': 'bank_transfer', 'reference': 'TRF-2026-0042', }, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() payment = resp.json()['data'] print(payment['id'], payment['amount']) ``` </Tab> <Tab value="Node.js"> ```javascript const res = await fetch( 'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 500.0, paid_on: '2026-05-20', payment_method: 'bank_transfer', reference: 'TRF-2026-0042', }), }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(data.id, data.amount); ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash curl -s -X POST \ https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 500.00, "paid_on": "2026-05-20", "payment_method": "bank_transfer", "reference": "TRF-2026-0042" }' | jq '.data' ``` </Tab> </Tabs> ## Pagos parciales y saldo [#pagos-parciales-y-saldo] El saldo en curso **no** vive en el objeto del pago — vive en la **factura**. Tras registrar uno o varios pagos, lee la factura (`GET /v1/invoices/{id}`) para ver cómo está: ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "object": "invoice", "status": "pending", "total": 1210.00, "paid_amount": 500.00, "pending_amount": 710.00, "payments": { "detail": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } ], "total": 500.00, "pending": 710.00 } } } ``` * `paid_amount` / `pending_amount` — los totales cobrado y pendiente. Siempre presentes, calculados a partir del ledger. * `payments.total` / `payments.pending` — las mismas dos cifras, reflejadas dentro del objeto `payments`. Siempre presentes. * `payments.detail` — el array de pagos individuales. Se materializa solo en el endpoint de **detalle** (`GET /v1/invoices/{id}`); en los endpoints de **listado** llega como `[]` (mientras `total` y `pending` siguen poblados) para que los listados sean ligeros. Usa el [sub-recurso](#list-payments) para obtener el detalle por separado. Cuando el último pago cierra el saldo (`pending_amount` llega a `0`), la factura pasa a `paid`. <Callout type="warn"> Un pago cuyo `amount` supera `pending_amount` se rechaza con `422` y `subcode: "payment_exceeds_pending_amount"` (`param: "amount"`). Un pago **exactamente igual** al importe pendiente es válido y salda la factura. Consulta [Errores](/es/guides/errors#business_rule_violation). </Callout> ### Listar pagos [#list-payments] `GET /v1/invoices/{id}/payments` devuelve el ledger completo de una factura, del más reciente al más antiguo. Una factura sin pagos devuelve `{ "data": [] }`, nunca un `404`. ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } ] } ``` ## Pagos de factura de compra [#pagos-de-factura-de-compra] Las facturas de compra mantienen su propio ledger (`total_retention`, la retención IRPF agregada, vive en el recurso de la factura de compra). El contrato es **asimétrico** respecto al de venta — léelo con atención antes de reutilizar código: * `POST /v1/purchase_invoices/{id}/payments` devuelve `201` con el **pago creado** bajo `data` (objeto `purchase_invoice_payment`), no la factura completa. * `GET /v1/purchase_invoices/{id}/payments` devuelve `{ "data": [...] }`, del más reciente al más antiguo. * El body añade un `bank_account_id` opcional (entero), y aquí `payment_method` es un **string libre** (máx. 30 caracteres), no el enum cerrado que se usa en el lado de venta. | Campo | Tipo | Requerido | Notas | | ----------------- | --------------------- | --------- | ----------------------------------------------------- | | `amount` | number | **Sí** | Mayor que `0`. No puede superar el importe pendiente. | | `paid_on` | string (`YYYY-MM-DD`) | **Sí** | Entre la fecha de emisión y hoy. | | `payment_method` | string | **Sí** | Texto libre, máx. 30 caracteres. | | `bank_account_id` | integer | No | Cuenta bancaria desde la que se hizo el pago. | | `reference` | string | No | Tu propia referencia. | | `notes` | string | No | Nota interna libre. | ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "object": "purchase_invoice_payment", "amount": 423.50, "paid_on": "2026-05-21", "payment_method": "transferencia", "bank_account_id": 12, "reference": "TRF-2026-0099", "notes": null, "created_at": "2026-05-21T09:00:00Z" } } ``` <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests resp = requests.post( 'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments', json={ 'amount': 423.50, 'paid_on': '2026-05-21', 'payment_method': 'transferencia', 'bank_account_id': 12, }, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() print(resp.json()['data']['id']) ``` </Tab> <Tab value="Node.js"> ```javascript const res = await fetch( 'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 423.5, paid_on: '2026-05-21', payment_method: 'transferencia', bank_account_id: 12, }), }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(data.id); ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash curl -s -X POST \ https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 423.50, "paid_on": "2026-05-21", "payment_method": "transferencia", "bank_account_id": 12 }' | jq '.data' ``` </Tab> </Tabs> <Callout type="info"> Las reglas de pago de la factura de compra (`BR-PUR-019`) se aplican como `422`: un importe por encima del saldo pendiente (`subcode: "payment_exceeds_pending_amount"`), una fecha fuera de `fecha_emisión … hoy` (`subcode: "invalid_payment_date"`), o un pago sobre una factura cancelada (`subcode: "purchase_invoice_not_payable"`). </Callout> ## Métodos de pago [#payment-methods] `GET /v1/payment-methods` devuelve el catálogo cerrado que respalda el campo `payment_method` de venta, cada uno con un `value` y una etiqueta legible (en español). Es un catálogo de enum global — no específico de empresa. ```json { "data": [ { "value": "bank_transfer", "label": "Transferencia bancaria" }, { "value": "direct_debit", "label": "Domiciliación bancaria" }, { "value": "cash", "label": "Efectivo" }, { "value": "credit_card", "label": "Tarjeta de crédito" }, { "value": "check", "label": "Cheque" }, { "value": "paypal", "label": "PayPal" }, { "value": "other", "label": "Otro" } ] } ``` Léelo una vez al arrancar y muestra las etiquetas en tu interfaz; devuelve el `value` en `payment_method`. ## Errores [#errores] * **`422` `payment_exceeds_pending_amount`** — el importe es mayor que el saldo pendiente (`param: "amount"`). Es una violación de regla de negocio, así que es `422`, nunca `409`. * **`409`** en un `POST` de pago se reserva para el envoltorio estándar de [idempotencia](/es/guides/idempotency) / conflicto (un `Idempotency-Key` reutilizado con un body distinto, o un conflicto de concurrencia) — no para los datos del pago en sí. Consulta [Errores](/es/guides/errors) para el envoltorio completo y el catálogo de códigos. --- # Presencia (/es/guides/presence) La **presencia** responde a dos preguntas en vivo: **¿quién trabaja ahora mismo?** y **¿quién está hoy en oficina y quién en remoto?** No es un CRUD sobre una tabla propia — es un **read-model derivado** compuesto a partir de tres fuentes: la **plantilla** de empleados, el **estado de fichaje** derivado del ledger de jornada, y el **horario** vigente. El estado de trabajo en vivo (`working`, `paused`, `finished`, `away`) y el indicador de llegada tarde se **computan al leer**, nunca se persisten. En la API v1, la presencia es de **solo lectura** (`presence:read`), bajo `https://api.factuarea.com/v1`. No existe el scope `presence:write`: declarar la presencialidad oficina/remoto es una tarea solo-portal que realiza el propio empleado. ## El panel de equipo en vivo [#live] `GET /v1/presence` devuelve el panel en vivo: un item por empleado activo con su estado de trabajo actual, desde cuándo lleva abierta la franja actual, y si llegó tarde respecto a su hora de entrada [planificada](/guides/work-schedules), más contadores agregados (trabajando, en pausa, ausente, en remoto). ```bash curl https://api.factuarea.com/v1/presence \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` El estado de trabajo se deriva del último evento de la franja abierta de cada empleado en el ledger: `clock_in`/`pause_end` → `working`, `pause_start` → `paused`, `clock_out` → `finished`, sin franja abierta → `away`. La llegada tarde compara el primer fichaje de entrada del día con la hora planificada leída del horario del empleado. ## Presencialidad diaria oficina/remoto [#daily] `GET /v1/presence/daily` lista la **presencialidad diaria** —oficina frente a remoto— con filtros por empleado, fecha o rango y paginación por cursor. `GET /v1/presence/{employee}` devuelve la presencia de un solo empleado por su `id` (UUID v7); un empleado de otra empresa devuelve `404`. ```bash curl -G https://api.factuarea.com/v1/presence/daily \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "date=2026-02-03" ``` La **presencialidad diaria** (oficina o remoto) es el único dato que la presencia guarda de verdad: un registro por empleado y día. Declararla de nuevo para el mismo día cambia la localización en vez de crear un duplicado. Consulta los esquemas en la [Referencia de API](/api-reference/presence/public-api.v1.presence.live). <Callout type="warn"> La presencia es de solo lectura en la API. Los empleados declaran si están en oficina o en remoto desde el portal — no hay endpoint público de escritura, así que una integración lee la presencia, no la fija. </Callout> ## Flujo típico [#flow] 1. Sondea `GET /v1/presence` para un tablero en vivo de quién trabaja, está en pausa o ausente. 2. Lee `GET /v1/presence/daily` para ver el reparto oficina/remoto de una fecha. 3. Profundiza en una persona con `GET /v1/presence/{employee}`. Como la presencia es derivada, las cifras siempre reflejan el estado actual del ledger y de los horarios — nunca necesitas mantener una tabla de presencia aparte sincronizada. ## Próximos pasos [#next] * [Fichajes](/guides/time-clock) — el ledger del que se deriva el estado en vivo. * [Horarios](/guides/work-schedules) — la hora planificada que usa el indicador de llegada tarde. --- # Inicio rápido (/es/guides/quickstart) Esta guía te lleva de una API key recién creada a una factura real (en sandbox) en cinco pasos. Cada llamada de abajo es **ejecutable copiando y pegando** contra una key `fact_test_` — sin emails reales, sin envío a la AEAT, sin consumir numeración de producción. Consulta [Modo de prueba & sandbox](/guides/test-mode) para ver qué desactiva el modo "test". <Callout type="info"> **¿Prefieres un SDK?** Si trabajas con TypeScript/Node o PHP, los [SDKs oficiales](/sdks) envuelven todo este flujo con reintentos integrados, idempotencia, paginación por cursor y errores tipados — `npm install @factuarea/sdk` o `composer require factuarea/factuarea-php`. Los pasos HTTP en crudo de abajo funcionan en cualquier lenguaje y muestran exactamente lo que el SDK envía por dentro. </Callout> <Callout type="info"> Ejecuta todo primero con una key **`fact_test_`**. La superficie de la API es idéntica en producción y en test — cuando tu flujo funcione de principio a fin, cambia el prefijo a `fact_live_` para pasar a producción. Consigue una key de test en [Settings → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys). </Callout> Exporta tu key una vez para que cada snippet la recoja: ```bash export FACTUAREA_API_KEY="fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" ``` La base URL es `https://api.factuarea.com/v1`. Autentícate con `Authorization: Bearer` (o con el header equivalente `X-API-Key`). Los identificadores son valores `id` opacos (UUID v7); los copias de una respuesta a la siguiente. <Steps> <Step> **Verifica tu key** `GET /v1/account` introspecciona la credencial: la empresa a la que pertenece, el plan, el estado del acceso a la API y los **scopes** y el **tier** de rate limit de la propia key (derivado del plan). Necesita el scope `account:read`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl https://api.factuarea.com/v1/account \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` </Tab> </Tabs> ```json { "data": { "object": "account", "company": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Acme Soluciones SL", "tax_id": "B12345678" }, "plan": { "slug": "empresario", "name": "Empresario" }, "addon": { "active": true, "in_grace": false, "expires_at": null }, "api_key": { "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "name": "Sandbox integration", "prefix": "fact_test_3pXnR2Vb", "scopes": [ "account:read", "series:read", "taxes:read", "clients:write", "invoices:write", "invoices:send", "pdfs:read" ], "tier": "pro", "created_at": "2026-05-01T09:30:00Z", "last_used_at": "2026-06-02T08:12:00Z", "expires_at": null } } } ``` Un `200` aquí significa que la key es válida y puedes ver exactamente qué scopes lleva. Si recibes `401 invalid_api_key`, revisa el valor; si un paso posterior falla con `403 insufficient_scope`, el array `scopes` de arriba te dice qué falta. </Step> <Step> **Consigue los ids que vas a necesitar** Una factura referencia una **serie** (su numeración) y cada línea referencia un **tipo impositivo**. Ambos son recursos existentes que listas una vez y reutilizas. **Un id de serie** `GET /v1/series` devuelve tus series de numeración. Elige una cuyo `document_type` sea `invoice` (la marcada como `is_default` es una opción segura). Necesita `series:read`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl "https://api.factuarea.com/v1/series?document_type=invoice" \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` </Tab> </Tabs> ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "object": "series", "code": "F-2026", "name": "Facturas 2026", "document_type": "invoice", "prefix": "F-2026-", "next_number": 46, "year_reset": true, "is_default": true, "is_active": true, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-20T11:30:00Z" } ], "has_more": false, "next_cursor": null } ``` Copia el `id` (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e`) — ese es tu `series_id`. **Un id de tipo impositivo** `GET /v1/taxes` devuelve el catálogo de impuestos (impuestos globales del sistema + los tuyos personalizados). Para una línea de factura española estándar quieres el tipo de IVA al 21% — busca `type: "vat"` y `rate: 21`. Necesita `taxes:read`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl "https://api.factuarea.com/v1/taxes?type=vat" \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` </Tab> </Tabs> ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", "object": "tax", "name": "IVA general 21%", "code": "IVA21", "rate": 21, "type": "vat", "applies_to": "both", "country": "ES", "is_default": true, "is_active": true, "is_system": true } ], "has_more": false, "next_cursor": null } ``` Copia este `id` — en una línea de factura va en `tax_rate_id`. <Callout type="info"> **`tax_rate_id` vs `tax_rate`.** En una línea puedes referenciar un tipo del catálogo por `tax_rate_id`, o saltarte la búsqueda y pasar el porcentaje numérico directamente como `tax_rate` (p. ej. `"tax_rate": 21`). Usa uno u otro por línea — `tax_rate_id` mantiene la línea vinculada a tu catálogo, `tax_rate` es un override inline rápido. </Callout> </Step> <Step> **Crea un cliente** La factura necesita alguien a quien facturar. El body mínimo de cliente es `name` más `tax_id` (el identificador fiscal español — NIF/CIF/NIE). Necesita `clients:write`. Esto es una escritura — envía una `Idempotency-Key` para que una petición reintentada nunca cree un cliente duplicado. Consulta [Idempotencia](/guides/idempotency). <Tabs items="['curl']"> <Tab value="curl"> ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Cliente Demo SL", "tax_id": "B98765432" }' ``` </Tab> </Tabs> ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "object": "client", "name": "Cliente Demo SL", "commercial_name": null, "tax_id": "B98765432", "vat_id": null, "email": null, "phone": null, "contact_person": null, "billing_emails": [], "address": { "line1": null, "postal_code": null, "city": null, "province": null, "country": null }, "coordinates": null, "notes": null, "metadata": {}, "is_active": true, "created_at": "2026-06-02T10:30:00Z", "updated_at": "2026-06-02T10:30:00Z" } } ``` (Los campos opcionales que no enviaste vuelven como `null`; `address` siempre es un objeto cuyas subclaves se rellenan a medida que las proporcionas.) Copia el `id` devuelto (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01`) — ese es tu `client_id`. </Step> <Step> **Crea la factura** Ahora combina los tres ids. `POST /v1/invoices` requiere `client_id`, `series_id`, `issued_on`, `due_on` y al menos una línea. Cada línea necesita `description`, `quantity` y `unit_price`; añade `tax_rate_id` (o `tax_rate`) para aplicar IVA. Opcionales por línea: `discount_percent` y `product_id`. Necesita `invoices:write`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "issued_on": "2026-06-02", "due_on": "2026-07-02", "lines": [ { "description": "Consultoría — junio 2026", "quantity": 10, "unit_price": 100, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", "discount_percent": 0 } ] }' ``` </Tab> </Tabs> La API calcula por ti los totales de la línea y del documento y devuelve el envoltorio de la factura. Una factura recién creada empieza como **borrador**: todavía sin `number` definitivo (`is_number_assigned: false`). ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42", "object": "invoice", "number": null, "is_number_assigned": false, "type": "F1", "series": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "code": "F-2026" }, "client": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Cliente Demo SL" }, "status": "draft", "issued_on": "2026-06-02", "due_on": "2026-07-02", "subtotal": 1000, "taxes_total": 210, "total": 1210, "currency": "EUR", "notes": null, "lines": [ { "object": "invoice_line", "description": "Consultoría — junio 2026", "product": null, "quantity": 10, "unit_price": 100, "tax_rate": 21, "discount_percent": 0, "subtotal": 1000, "taxes": 210, "total": 1210 } ], "metadata": {}, "operation_regime": "general", "verifactu_status": "not_applicable", "is_corrective": false, "corrective": null, "payment": null, "public_link": null, "substituted_by": null, "recurring": null, "paid_at": null, "paid_on": null, "sent_at": null, "voided_at": null, "void_reason": null, "created_at": "2026-06-02T10:31:00Z", "updated_at": "2026-06-02T10:31:00Z" } } ``` Fíjate en los campos monetarios calculados: el `subtotal` de la línea (`10 × 100 = 1000`), sus `taxes` (`21%` de `1000 = 210`) y el `total` (`1210`), agregados en el `subtotal` / `taxes_total` / `total` de la factura. Copia el `id` de la factura (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42`) para el siguiente paso. </Step> <Step> **Obtén el PDF y envíalo** Con el `id` de la factura puedes descargar su PDF y enviárselo por email al cliente. `GET /v1/invoices/{id}/pdf` transmite el PDF binario (`application/pdf`). Necesita `pdfs:read`. Guárdalo directamente a un fichero con la opción `-o` de curl: <Tabs items="['curl']"> <Tab value="curl"> ```bash curl https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/pdf \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -o invoice.pdf ``` </Tab> </Tabs> `POST /v1/invoices/{id}/send` envía la factura por email al cliente. Sin body usa el email del cliente registrado en ficha; puedes sobreescribir el destinatario y la copia con `to`, `cc`, `bcc`, `subject` y `body`. Necesita `invoices:send`. <Callout type="info"> Como estás con una key `fact_test_`, el email **no se entrega** a ningún destinatario real (los efectos del sandbox están desactivados). La llamada igualmente tiene éxito y la factura transiciona como lo haría en producción — perfecto para montar tu flujo sin hacer spam a nadie. </Callout> <Tabs items="['curl']"> <Tab value="curl"> ```bash curl -X POST https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/send \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "to": "demo@example.com", "subject": "Tu factura de Acme Soluciones SL" }' ``` </Tab> </Tabs> La respuesta es el envoltorio actualizado de la factura (con la misma forma que arriba), ahora con `sent_at` poblado. </Step> </Steps> ## Y ya está [#y-ya-está] Verificaste una key, descubriste los ids que necesita, creaste un cliente, emitiste una factura con totales calculados en el servidor y la enviaste — todo contra un sandbox aislado. A partir de aquí, apunta el mismo código a una key `fact_live_` para operar sobre tu empresa real. <Cards> <Card icon="<Package />" title="SDKs oficiales" href="/sdks"> Ejecuta este mismo flujo con @factuarea/sdk (TypeScript) o factuarea/factuarea-php — reintentos, idempotencia, paginación y errores tipados incluidos. </Card> <Card icon="<FlaskConical />" title="Modo de prueba y sandbox" href="/guides/test-mode"> Qué desactiva una key fact\_test\_, cómo se aísla la empresa sandbox, y cómo promocionar tu integración a producción. </Card> </Cards> --- # Límites de peticiones (/es/guides/rate-limits) La API pública aplica dos niveles de cuota para garantizar la equidad entre tenants y proteger el backend frente a picos: 1. **Cuota por minuto** (ventana deslizante). 2. **Cuota mensual** (calendario natural, se reinicia el día 1 a las 00:00 Europe/Madrid). Los límites dependen del **tier** de tu API key. El tier se **deriva del plan de Factuarea de tu empresa** (o de un [boost de capacidad](#capacity-boost) activo cuando es superior) — nunca se fija por clave ni por petición, y se actualiza automáticamente cuando tu plan cambia. ## Niveles [#niveles] | Tier | Por minuto | Por mes | Incluido con | | ----------- | ------------- | ------------- | --------------------------------------------- | | **Free** | 10 rpm | 100 requests | El trial de 10 días. | | **Starter** | 30 rpm | 5,000 | El plan Emprendedor. | | **Pro** | 300 rpm | 50,000 | El plan Empresario. | | **Scale** | Personalizado | Personalizado | El plan Enterprise (o un boost de capacidad). | Los tiers son acumulativos: una vez agotada la cuota mensual recibes `429 rate_limit_exceeded` hasta el día 1 del mes siguiente. La cuota por minuto se reinicia con una ventana deslizante. ## Boost de capacidad [#capacity-boost] Si necesitas más capacidad de API sin cambiar de plan, suscríbete a un **boost de capacidad** desde el dashboard: un tier **estrictamente superior** al que otorga tu plan (por ejemplo, Starter → Pro). Mientras el boost está activo, todas tus claves usan el tier del boost. Comprar un tier igual o inferior al de tu plan devuelve `422 boost_not_applicable`. ## Ventana deslizante [#ventana-deslizante] El cubo por minuto **no** es una ventana fija de "60 segundos desde las 12:00". Es una ventana deslizante: en cualquier momento, la API cuenta cuántas peticiones aceptadas hay en los últimos 60 segundos para tu clave. Cuando el contador iguala al límite, las peticiones siguientes responden `429` hasta que pase suficiente tiempo para que las primeras peticiones "salgan" de la ventana. <Callout type="info"> **Por qué**: no hay un "minuto de gracia" cada 60 segundos en el que pudieras enviar el doble del límite. Más justo y más estable bajo tráfico real. </Callout> ## Cabeceras de respuesta [#cabeceras-de-respuesta] Toda respuesta (incluido 429) incluye: | Cabecera | Significado | | ----------------------- | ------------------------------------------------------------------------------ | | `X-RateLimit-Limit` | Límite por minuto de tu tier. | | `X-RateLimit-Remaining` | Peticiones restantes en la ventana actual. | | `X-RateLimit-Reset` | Timestamp UNIX en el que se libera un hueco (una petición sale de la ventana). | | `Retry-After` | Solo en `429`. Segundos hasta que puedas reintentar. | Ejemplo de cabeceras en una respuesta `200`: ```http HTTP/1.1 200 OK X-RateLimit-Limit: 300 X-RateLimit-Remaining: 287 X-RateLimit-Reset: 1747314060 ``` Y en un `429`: ```http HTTP/1.1 429 Too Many Requests Retry-After: 7 X-RateLimit-Limit: 30 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1747314007 ``` ## Código de error [#código-de-error] ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Has superado el límite de peticiones. Vuelve a intentarlo en unos segundos.", "request_id": "req_..." } } ``` Superar la cuota por minuto o la mensual responde `429` con `type: rate_limit_error` y `code: rate_limit_exceeded`. La cabecera `Retry-After` (y el mensaje) te indican cuánto debes esperar. Los fallos de autenticación repetidos se limitan por separado con `code: too_many_auth_failures`. ## Buenas prácticas [#buenas-prácticas] ### 1. Respeta Retry-After [#1-respeta-retry-after] ```python import time, requests def call_with_retry(url, **kwargs): while True: resp = requests.get(url, **kwargs) if resp.status_code != 429: return resp sleep = int(resp.headers.get('Retry-After', 1)) time.sleep(sleep) ``` ### 2. Back-off exponencial con jitter [#2-back-off-exponencial-con-jitter] Para `5xx`, donde no hay `Retry-After`: ```python import random, time def backoff(attempt): return min(60, (2 ** attempt) * 0.1 + random.uniform(0, 0.5)) for attempt in range(5): resp = requests.get(url) if resp.status_code < 500: break time.sleep(backoff(attempt)) ``` ### 3. Monitoriza X-RateLimit-Remaining [#3-monitoriza-x-ratelimit-remaining] Si tu integración se acerca de forma consistente al 10% del límite, considera: * Subir de tier. * Agrupar en lotes: en lugar de N POSTs, agrega y haz 1 POST. * Cachear lecturas frecuentes (productos, impuestos, series). * Suscribirte a webhooks en lugar de hacer polling. ### 4. Webhooks > polling [#4-webhooks--polling] Si haces polling de `/v1/invoices?status=paid` cada minuto para detectar pagos consumes 30 rpm solo para eso. Suscríbete al evento `invoice.paid` y reduce eso a 0 peticiones. ### 5. Claves por integración [#5-claves-por-integración] Si tienes dos integraciones (un dashboard interno + un cron de exportación), crea **dos claves distintas**: cada clave tiene sus propios cubos por minuto y mensual, así un cron pesado no agota el presupuesto de un dashboard interactivo. ## Cuotas administrativas [#cuotas-administrativas] Algunos endpoints tienen cuotas adicionales **independientes** del rate limit principal: | Endpoint | Cuota | | ----------------------------- | --------------------------------------------------- | | `POST /v1/webhook_endpoints` | Número limitado de endpoints por empresa. | | `POST /v1/invoices/{id}/send` | Limitado por factura para evitar emails duplicados. | | `GET /v1/invoices/{id}/pdf` | Generaciones de PDF limitadas por minuto. | Estos límites responden `429` con `type: rate_limit_error` y un mensaje específico. ## Subida de tier [#subida-de-tier] Cambiar de tier no requiere rotar claves. Cuando tu plan cambia (o se activa un boost de capacidad): 1. Las nuevas cuotas aplican de inmediato. 2. La cuota mensual consumida en el tier anterior **no se reinicia**: solo crece el tope mensual. 3. Las claves existentes conservan su `id`; el nuevo tier se aplica a todas automáticamente. --- # Facturas recurrentes (/es/guides/recurring-invoices) Una **factura recurrente** es una plantilla más una cadencia: Factuarea genera una factura real en cada ejecución programada. Esta guía cubre los controles que van más allá del create/update básico — omitir un ciclo, arrancar una recurrencia a partir de una factura existente, el envío automático por correo, la previsualización del documento calculado y los campos fiscales por línea que exige VeriFactu. Todos los endpoints de abajo viven bajo `https://api.factuarea.com/v1` y usan el mismo [envoltorio de error](/guides/errors), [paginación por cursor](/guides/pagination) y [scopes](/guides/scopes-and-irreversibility) que el resto de la API. ## Omitir la próxima generación [#omitir-la-próxima-generación] ```http POST /v1/recurring_invoices/{recurring_invoice}/skip ``` Avanza `next_run_at` exactamente un periodo **sin generar una factura** para el ciclo actual. La ocurrencia omitida **no** cuenta para `max_occurrences` — el contador de facturas generadas no se mueve. Úsalo para saltarte un periodo de facturación (festivos, un cliente en pausa) manteniendo intacto el calendario. Requiere el scope `recurring_invoices:write`. Devuelve `200` con el recurso de la factura recurrente (fíjate en el `next_run_at` avanzado). Una recurrencia cancelada o completada responde `422`; una factura recurrente que pertenece a otra empresa responde `404`. ```bash curl -X POST https://api.factuarea.com/v1/recurring_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/skip \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` <Callout type="info"> Omitir registra una entrada `skipped` en la actividad de la factura recurrente — el historial muestra que el ciclo se omitió a propósito, no que se perdió. </Callout> ## Crear una recurrencia desde una factura existente [#crear-una-recurrencia-desde-una-factura-existente] ```http POST /v1/invoices/{invoice}/create-recurring ``` Copia las **líneas, el cliente y la serie** de una factura origen en una factura recurrente nueva (con su propio UUID v7) y aplica la cadencia indicada en el cuerpo. La factura origen queda intacta. Requiere el scope `recurring_invoices:write`. | Campo | Tipo | Obligatorio | Notas | | ------------------ | --------------------- | ----------- | ------------------------------------------------------------------------------ | | `frequency` | string | sí | `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual`, `yearly`. | | `start_on` | string (`YYYY-MM-DD`) | sí | Primera ejecución programada. | | `end_on` | string (`YYYY-MM-DD`) | no | Última ejecución permitida. | | `name` | string | no | Etiqueta de la recurrencia (≤255). | | `description` | string | no | | | `notes` | string | no | | | `metadata` | object | no | Tus pares clave/valor. | | `holiday_handling` | string | no | Cómo desplazar una ejecución que cae en festivo. | | `days_before_due` | integer | no | Desfase de vencimiento de cada factura generada. | | `max_occurrences` | integer | no | Detener tras N facturas generadas. | | `auto_delivery` | object | no | Ver [Envío automático](#envío-automático). | Devuelve `201` con la nueva factura recurrente y una cabecera `Location` que apunta a ella. Una factura origen que pertenece a otra empresa responde `404`. ```bash curl -X POST https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/create-recurring \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "frequency": "monthly", "start_on": "2026-07-01", "max_occurrences": 12, "auto_delivery": { "send_automatically": true, "recipients": ["billing@acme.example"] } }' ``` ## Envío automático [#envío-automático] El objeto `auto_delivery` — disponible al crear y actualizar una recurrente y en `create-recurring` — manda por correo cada factura generada de forma automática. | Campo | Tipo | Notas | | -------------------- | --------- | ----------------------------------------------------------- | | `send_automatically` | boolean | Interruptor maestro. `false` desactiva el envío. | | `recipients` | string\[] | Destinatarios principales (email). | | `cc` | string\[] | Destinatarios en copia (email). | | `subject` | string | Asunto personalizado (≤255). `null` usa el predeterminado. | | `body` | string | Cuerpo personalizado (≤5000). `null` usa el predeterminado. | Cuando `send_automatically` es `true`, cada factura generada se manda por correo a `recipients` (con `cc` opcional) usando `subject`/`body`. Poner `send_automatically` a `false` desactiva el envío. Una lista `recipients` vacía con `send_automatically: true` se rechaza con `422` — no hay a quién enviar. ```json { "auto_delivery": { "send_automatically": true, "recipients": ["billing@acme.example"], "cc": ["copy@acme.example"], "subject": "Tu factura mensual", "body": "Hola, aquí tienes tu factura de este periodo." } } ``` ## Previsualizar el documento calculado [#previsualizar-el-documento-calculado] ```http GET /v1/recurring_invoices/{recurring_invoice}/preview ``` Sin `expand`, `preview` devuelve solo la **previsión de fechas** — las próximas fechas de ejecución (usa `count` para controlar cuántas). Pasa `expand=document` para calcular **además** el próximo documento en seco: la respuesta añade un bloque `next_invoice` con las `lines` resueltas y los `totals` (`subtotal`, `tax`, `total`), construidos desde `template_data` **sin persistir nada**. Úsalo para mostrarle al cliente qué contendrá exactamente la próxima factura antes de emitirla. ```bash curl -G https://api.factuarea.com/v1/recurring_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/preview \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "expand=document" ``` ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "recurring_invoice" }, "next_invoice": { "lines": [ { "description": "Cuota mensual", "quantity": 1, "unit_price": 500, "subtotal": 500 } ], "totals": { "subtotal": 500, "tax": 105, "total": 605 } } } ``` <Callout type="info"> `preview` nunca crea una factura. Es una lectura pura — los totales en seco se calculan en memoria desde `template_data`. </Callout> ## Campos fiscales por línea [#campos-fiscales-por-línea] Cada línea de `template_data` acepta los campos fiscales que necesitan VeriFactu y el modelo tributario español. Se trasladan a cada factura generada. | Campo | Tipo | Notas | | ------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `exemption_reason` | string | Causa de exención / no sujeción según LIVA. Uno de `E1`–`E6`, `N1`, `N2`. `null` si no se informa. | | `regime_key` | string | Clave de régimen VeriFactu (`ClaveRegimen`, lista L8.1 de la AEAT). `null` si no se informa. | | `retention` | number | Porcentaje de retención IRPF (0–100). | | `retention_rate_id` | string (UUID v7) | Impuesto del catálogo aplicado como retención IRPF. Opaco — no cambia el cálculo de `taxes`/`total` (lo hace el porcentaje plano `retention`). | | `surcharge` | number | Porcentaje de recargo de equivalencia (0–100). | | `surcharge_rate_id` | string (UUID v7) | Impuesto del catálogo aplicado como recargo de equivalencia. Opaco — el porcentaje plano `surcharge` gobierna el cálculo. | El recargo de equivalencia va ligado por ley al IVA de la línea. Los únicos pares legales `tax_rate` → `surcharge` son: | IVA (`tax_rate`) | Recargo (`surcharge`) | | ---------------- | --------------------- | | `21` | `5.2` | | `10` | `1.4` | | `4` | `0.5` | Enviar un `exemption_reason`, `regime_key`, `retention_rate_id` o `surcharge_rate_id` fuera de su catálogo responde `422` con una lista `allowed_values` en el error. Un par IVA↔recargo ilegal (p.ej. `21` con `1.4`) también se rechaza con `422`. ```json { "template_data": { "lines": [ { "description": "Consultoría", "quantity": 1, "unit_price": 1000, "tax_rate": 21, "surcharge": 5.2, "retention": 15, "regime_key": "01", "exemption_reason": null } ] } } ``` --- # Claves de régimen (/es/guides/regime-keys) La facturación española sobrecarga la palabra *régimen*. Tres conceptos distintos la comparten, viven en niveles diferentes del documento y solo uno de ellos es algo que envíes por la API pública: | Concepto | Nivel | ¿Lo fijas tú en la v1? | Determina | | ----------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------- | --------------------------------------------------------------------------------------- | | **Régimen de operación** — interior, intracomunitario, exportación, inversión del sujeto pasivo | Cabecera de la factura | **No.** Solo lectura. | La calificación AEAT de la operación (`S1`, `S2`, `E5`, `E2`). | | **Clave de régimen** (`ClaveRegimen`, lista AEAT L8.1) | Línea de factura | **Sí** — `lines[].regime_key` | El código de régimen especial declarado para esa línea. | | **Régimen indirecto** — IVA, IGIC, IPSI | Línea de factura | Sí — `lines[].indirect_tax_regime` | Qué impuesto aplica siquiera. Ver [Impuestos territoriales](/guides/territorial-taxes). | Confundir los dos primeros es, con diferencia, la causa más habitual de una factura mal calificada. Esta página los separa. ## Cuándo aplica [#when] Siempre: toda factura emitida declara una calificación y, para casi todos los regímenes fiscales, una clave de régimen. Lo que varía es si dejas ambas a la derivación o las declaras explícitamente por línea. Declara una clave de régimen explícita cuando la operación pertenezca a un régimen especial — bienes usados, agencias de viaje, criterio de caja, agricultura, recargo de equivalencia, ventas a distancia por ventanilla única. La derivación por cabecera solo produce el régimen general o la exportación, así que **la granularidad del catálogo completo solo se alcanza por línea**. ## El catálogo cerrado [#catalog] `lines[].regime_key` acepta exactamente estos diecisiete códigos de dos dígitos, de la lista `ClaveRegimen` L8.1 de la AEAT ([`BR-INV-031`](#traceability)). Cualquier otro valor responde `422` con la lista completa en `allowed_values`. | Código | Régimen | | ------ | ------------------------------------------------------------------------------------------------------------------------------- | | `01` | Régimen general. | | `02` | Exportación. | | `03` | Bienes usados, objetos de arte, antigüedades y objetos de colección (REBU). | | `04` | Oro de inversión. | | `05` | Agencias de viajes. | | `06` | Grupo de entidades en IVA, nivel avanzado. | | `07` | Régimen especial del criterio de caja. | | `08` | Operaciones sujetas al IPSI o al IGIC. | | `09` | Facturación de prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena. | | `10` | Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros. | | `11` | Operaciones de arrendamiento de local de negocio sujetas a retención. | | `14` | Factura con IVA pendiente de devengo — certificaciones de obra cuyo destinatario sea una Administración Pública. | | `15` | Factura con IVA pendiente de devengo — operaciones de tracto sucesivo. | | `17` | Operaciones acogidas al Capítulo XI del Título IX — ventanilla única (OSS e IOSS). | | `18` | Recargo de equivalencia. | | `19` | Agricultura, ganadería y pesca (REAGYP). | | `20` | Régimen simplificado. | Los números `12`, `13` y `16` faltan a propósito — no forman parte de la lista, y enviarlos se rechaza igual que cualquier otro valor fuera del catálogo. ## Qué envía la API [#api] `regime_key` es un campo **opcional, por línea y aditivo** de [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) y [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). Una línea que lo omite cae a la clave derivada de la cabecera de la factura: ```json { "lines": [ { "description": "Reventa de maquinaria de ocasión", "quantity": 1, "unit_price": 100, "regime_key": "03" }, { "description": "Servicio de instalación", "quantity": 1, "unit_price": 50, "tax_rate": 21 } ] } ``` La primera línea declara el régimen de bienes usados; la segunda, sin clave, se deriva de la cabecera. Omitir el campo en todas las líneas reproduce exactamente el comportamiento que existía antes de introducir las claves por línea, **huella incluida** — que es la razón por la que el campo es aditivo y no obligatorio ([`BR-INV-031`](#traceability)). Tres de los cuatro ejemplos publicados de creación de factura —`b2c`, `intracomunitario_bienes` y `con_irpf`, en el desplegable de ejemplos del cuerpo de petición— declaran `regime_key: "01"` de forma explícita en lugar de apoyarse en el valor derivado. El cuarto, `b2b_nacional`, lo omite y deja que el régimen lo ponga la cabecera, lo cual es igual de válido. Copia la costumbre explícita: una clave por línea se documenta a sí misma y sobrevive a un cambio en la derivación por cabecera. ### El régimen de cabecera es de solo lectura en la v1 [#header-readonly] El objeto factura devuelve `operation_regime`, y ni la operación de creación ni la de actualización lo aceptan. **Toda factura creada por la API pública nace bajo el régimen general.** La causa de exención a nivel de documento —`exemption_reason` en el objeto factura— es de solo lectura por el mismo motivo. La consecuencia es concreta y conviene decirla sin rodeos: la calificación derivada de la cabecera será `S1` en cualquier factura creada por la v1, así que **la exención y la no sujeción deben declararse por línea**, con `lines[].exemption_reason`. Es exactamente lo que hace el ejemplo `intracomunitario_bienes` —`tax_rate: 0` más `exemption_reason: "E5"`— en lugar de apoyarse en un régimen de cabecera que no puede fijar. Ver [Clasificación fiscal y exenciones por línea](/guides/line-tax-classification-and-exemptions) para el catálogo de línea, y [Alcance y limitaciones](/guides/scope-and-limitations) para qué permite y qué no esta frontera. ### El catálogo legible por máquina [#tax-catalog] `GET /v1/tax-catalog` (scope `taxes:read`) publica los catálogos fiscales que describe esta página — regímenes indirectos con sus tipos válidos y sus códigos AEAT, regímenes de operación con sus menciones legales, causas de exención con su artículo de la LIVA, tipos de retención del sistema y los pares legales de IVA y recargo — con etiquetas en español, inglés y catalán en cada respuesta. ```bash curl https://api.factuarea.com/v1/tax-catalog \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Dos propiedades hacen seguro cachearlo de forma agresiva ([`BR-TAX-028`](#traceability)): es **idéntico para todas las empresas** —la consulta no lleva identificador de empresa y ninguna de sus fuentes está acotada a un tenant, así que dos API keys reciben cuerpos idénticos byte a byte y por tanto el mismo `ETag`— y se deriva de los objetos de valor cerrados del código y no de una lista copiada, de modo que un caso nuevo aparece automáticamente en vez de desincronizarse en silencio. <Callout type="info"> Los tipos de retención se publican en **positivo**, con el signo que sea con el que estén almacenados. El bloque se filtra por «impuesto del sistema», no por «activo»: un tipo que una empresa haya desactivado sigue formando parte del catálogo legal, y un impuesto propio creado por un tenant no aparece nunca en él. </Callout> ## Qué sale en el PDF [#pdf] La clave de régimen en sí **no se imprime**. Lo que ve quien lee el documento es la mención legal derivada del régimen de operación — la referencia al art. 25 LIVA en una entrega intracomunitaria, al art. 21 en una operación con terceros países, al art. 84.Uno.2 en la inversión del sujeto pasivo — y nada en absoluto para el régimen general, que no necesita mención ([`BR-TAX-024`](#traceability)). Como esas menciones derivan del régimen de cabecera, y el régimen de cabecera no se puede fijar en la v1, una factura creada por la API pública no imprime ninguna mención automática de régimen. Usa `notes` si el documento necesita declarar la exención en prosa. ## Qué llega a la AEAT [#aeat] Viajan dos campos distintos por cada grupo de desglose, y responden a preguntas distintas. **La calificación** responde a «¿qué clase de operación es esta?», y se deriva del régimen de cabecera ([`BR-VFC-029`](#traceability)): | Régimen de operación de cabecera | Calificación | Qué recibe la AEAT | | -------------------------------- | ------------ | ---------------------------------------------------------------------------------- | | General | `S1` | Sujeta y no exenta, cuota de IVA `base × tipo`. | | Inversión del sujeto pasivo | `S2` | Sujeta y **no** exenta, cuota forzada a **0** — la autorrepercute el destinatario. | | Intracomunitario | `E5` | Sujeta y exenta, art. 25 LIVA. | | Importación o exportación | `E2` | Sujeta y exenta, art. 21 LIVA. | Los códigos `E1`, `E3`, `E4` y `E6` existen en el catálogo de la AEAT pero esta derivación no los produce nunca: solo se alcanzan como causa de exención de **línea**. Una línea que declare una gana al valor derivado de la cabecera ([`BR-INV-032`](#traceability)). **La clave de régimen** responde a «¿bajo qué régimen especial?», y *no* se emite de forma incondicional ([`BR-VFC-035`](#traceability)): * Bajo **IPSI** no se emite nunca. Las reglas de validación de la AEAT son explícitas en que este impuesto no lleva `ClaveRegimen`. * Bajo **IVA** e **IGIC** la clave se deriva, con un valor general conservador, y nunca se fija a fuego a `08`. El código `08` corresponde a un emisor peninsular cuya operación se localiza en Canarias, Ceuta o Melilla — no a un emisor establecido allí, que declara su propio impuesto con su propia lista. * Una causa de exención a nivel de documento que lleve su propia clave de régimen especial —bienes usados, agricultura, agencias de viaje, criterio de caja, recargo de equivalencia— tiene prioridad sobre ese valor por defecto. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-INV-031` — el catálogo cerrado L8.1 de `lines[].regime_key`, su valor derivado de la cabecera y la invariante de huella idéntica. * `BR-INV-032` — las causas de exención de línea ganando a la calificación derivada de la cabecera. * `BR-VFC-029` — el mapa de calificaciones desde el régimen de operación de cabecera, y el hecho de que por esa vía solo se alcanzan `E5` y `E2`. * `BR-VFC-035` — cómo se deriva `ClaveRegimen`: nunca fijada a fuego a `08`, nunca emitida para IPSI, prioridad de la clave especial de la causa de exención. * `BR-TAX-024` — la causa de exención a nivel de documento y la mención legal automática. * `BR-TAX-028` — el catálogo fiscal público: sus cinco fuentes, su independencia del tenant y la publicación en positivo de los tipos de retención. --- # Alcance y limitaciones (/es/guides/scope-and-limitations) Toda plataforma tiene fronteras. Una frontera que puedes leer antes de integrar es una decisión de diseño; una que descubres en producción es un defecto. Esta página es la única lista canónica — ninguna otra guía mantiene la suya. Cada fila declara el **escenario**, su **estado** y el **workaround**: la alternativa disponible hoy, o una declaración explícita de que no la hay. Hay exactamente dos estados, porque una tercera categoría difusa es lo que convierte páginas como esta en mero adorno: * **Por diseño** — no lo vamos a construir. La alternativa está aquí. * **En roadmap** — aplazado, no descartado. <Callout type="info"> Verificado el **31 de julio de 2026** contra la **v1** de la API. Una fila cuyo escenario pase a estar soportado se retira en el mismo cambio que lo implementa, en lugar de quedarse ahí como limitación obsoleta. </Callout> ## Limitaciones verificadas contra el código [#gaps] | Escenario | Estado | Workaround | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Autofactura** — el destinatario expide la factura en nombre del proveedor | Por diseño | No está modelada. El proveedor expide su propia factura. Si operas ambas partes, emítela desde la cuenta del proveedor. | | **Factura expedida por un tercero** | Por diseño | El campo AEAT de expedición por tercero no se emite. Una asesoría que opera la cuenta de un cliente emite desde esa cuenta con [`X-Active-Profile`](/guides/acting-on-behalf); la factura se declara como expedida por la propia empresa. | | **Multidivisa** | Por diseño | El contrato v1 expone el euro, fijo: `currency` vale siempre `EUR` y no existe columna de divisa. **Filtrar un listado por cualquier otra divisa devuelve una página vacía, no un error.** Factura en euros y convierte fuera de Factuarea. | | **TicketBAI / Batuz (País Vasco)** | Por diseño | **Sin alternativa dentro de Factuarea.** Los sistemas forales vascos usan esquemas, certificados y endpoints distintos, y exigen su propio software homologado. Las empresas con domicilio fiscal vasco reciben el aviso durante el onboarding. | | **Inversión del sujeto pasivo, y cualquier régimen de operación de cabecera, declarados por la API** | Por diseño | El `operation_regime` de cabecera es de solo lectura en la v1 —ni la creación ni la actualización lo aceptan—, así que toda factura creada por la API nace en régimen general y se califica `S1`. La exención y la no sujeción se declaran por línea con `lines[].exemption_reason`, pero **la inversión del sujeto pasivo es la calificación `S2` y no tiene equivalente de línea**: emite esas facturas desde el panel. Ver [Clientes internacionales](/guides/international-customers#map). | | **Suplidos fuera de la factura emitida** — presupuestos, proformas, albaranes, facturas de compra, plantillas de recurrentes | Por diseño | Solo la factura emitida modela los suplidos. Incluye el importe como línea ordinaria en el documento previo, y fija `line_type` en la factura resultante **mientras siga en borrador** — la operación de actualización lo acepta. | | **Suplidos en el XML de Facturae y UBL** — el importe a pagar del XML es el total fiscal, no el importe debido | En roadmap | La base imponible y las cuotas salen correctas —el suplido está bien excluido—, pero el importe a pagar se queda corto por ese importe y ningún elemento del XML transporta la diferencia. **No remitas por [FACe](/guides/face-invoicing) una factura con líneas de suplido** mientras no se mapee el bloque nativo de Facturae 3.2.2: factura el suplido fuera de ese canal. | | **Suplidos en las cifras agregadas de cartera** — el `pending_amount` de [`GET /v1/invoices/stats`](/api-reference/invoices/public-api.v1.invoices.stats), el informe de aging y el de mayores deudores | Por diseño | Esos agregados miden **volumen facturado**, la misma magnitud que declara la declaración anual de operaciones con terceros, y tampoco han restado nunca los cobros parciales. Para el importe realmente debido, lee el `pending_amount` de cada factura, que sí mide contra el importe a pagar. | ## Diferencias deliberadas con otras plataformas [#deliberate] Son decisiones de producto conscientes, no huecos. Cada una existe porque la alternativa que elegimos es mejor para quien integra que el patrón que se nos pide. | Escenario | Estado | Workaround | | ------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paginación por desplazamiento** con número de páginas y salto a la página N | Por diseño | **Paginación por cursor** al estilo de Stripe: `limit` con límites validados, más `starting_after` **o** `ending_before` (mutuamente excluyentes). Las respuestas llevan `has_more` y `next_cursor`, y `next_cursor` vale `null` cuando `has_more` es falso. Ver [Paginación](/guides/pagination). | | **Envelope de error dual permanente** — nuestro envelope y RFC 9457 en el mismo cuerpo, siempre | Por diseño | **Negociación de contenido.** `Accept: application/problem+json` devuelve RFC 9457 puro; cualquier otro caso —`application/json`, `*/*`, sin cabecera `Accept`— devuelve nuestro envelope. Ver [Errores](/guides/errors). | | **Atomicidad total en la creación masiva** — una fila mala rechaza el lote entero | Por diseño | **Éxito parcial.** La respuesta lleva `{dry_run, total, successful, failed, results, failures}`, donde cada fallo identifica su fila por un `index` que empieza en cero, con su propio código de error. Importa 480 de 500 y arregla las 20. Ver [Operaciones masivas](/guides/bulk-operations). | | **Totales de línea obligatorios en la petición** (`line_total`, `taxable_base`) | Por diseño | `line_total` es una **suma de control opcional verificada**: se compara con el total calculado con una tolerancia de un céntimo y se descarta — nunca se persiste, nunca se devuelve. No tienes que replicar nuestro motor de cálculo. Ver [Suplidos](/guides/disbursements#checksum). | | **Representación o apoderamiento por terceros** — endpoints de apoderado, documentos de autorización firmados | Por diseño | Cada empresa sube **su propio certificado**, que debe coincidir con su propio NIF, se valida por estructura y tamaño, y cuya contraseña se guarda cifrada. | | **Sustituir facturas simplificadas en dos pasos** — una rectificativa más una factura completa nueva | Por diseño | **Un solo paso nativo:** `POST /v1/invoices/substitute-simplified` emite la factura sustitutiva agregando varias simplificadas. Ver [Facturas simplificadas o completas](/guides/simplified-vs-full-invoices#substitute). | | **SDK de Python** | En roadmap | Genera un cliente a partir del documento OpenAPI publicado, o usa los SDK de [TypeScript](/sdks/typescript) o [PHP](/sdks/php), el [CLI](/cli) o el [servidor MCP](/mcp). | ## Capacidades que puedes dar por ausentes [#capabilities] Cuatro cosas que Factuarea sí hace y que quien llega de otras plataformas espera habitualmente no encontrar: | Capacidad | Dónde | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Factura sustitutiva de simplificadas, en una sola llamada** — agregar varios tiques en una factura completa sin rectificativa previa | [Facturas simplificadas o completas](/guides/simplified-vs-full-invoices#substitute) · [`POST /v1/invoices/substitute-simplified`](/api-reference/invoices/public-api.v1.invoices.substitute_simplified) | | **Subsanación de registros VeriFactu rechazados, expuesta en la API pública** — reparar una declaración rechazada sin anular la factura | [Subsanación de registros VeriFactu](/guides/verifactu-subsanacion) · [`POST /v1/verifactu/records/{id}/subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) | | **Rectificativa por diferencias con base imponible negativa** — la forma fiscalmente correcta de expresar un abono | [Facturas rectificativas](/guides/corrective-invoices#nature) | | **Catálogo fiscal AEAT consultable por la API** — regímenes indirectos, regímenes de operación, causas de exención con su artículo de la LIVA, tipos de retención y los pares legales de IVA y recargo, en tres idiomas | [Claves de régimen](/guides/regime-keys#tax-catalog) · `GET /v1/tax-catalog` | Ninguna de ellas está anunciada ni en desarrollo: las cuatro son operaciones vivas hoy. ## Dónde vive el razonamiento fiscal [#see-also] Esta página lista fronteras. Las guías que explican las reglas que hay detrás: <Cards> <Card title="Estados de envío VeriFactu" href="/guides/verifactu-submission-states" description="El ciclo de vida del registro, reintento y subsanación." /> <Card title="Facturas rectificativas" href="/guides/corrective-invoices" description="R1–R5, sustitución frente a diferencias." /> <Card title="Claves de régimen" href="/guides/regime-keys" description="Calificación de cabecera y catálogo de régimen por línea." /> <Card title="Impuestos territoriales" href="/guides/territorial-taxes" description="IVA, IGIC e IPSI." /> <Card title="Suplidos" href="/guides/disbursements" description="Importes pagados por cuenta del cliente." /> <Card title="Clientes internacionales" href="/guides/international-customers" description="Identificación alternativa y mapa de escenarios." /> </Cards> ## Trazabilidad [#traceability] Esta página documenta la **ausencia** de comportamiento, algo que ninguna regla de negocio puede afirmar. Sus filas se anclan, por tanto, de forma distinta a las demás guías fiscales: a un punto verificado del código, a la decisión registrada para la plataforma o —cuando sí existe una regla de negocio— a esa regla. **Limitaciones verificadas contra el código:** | Fila | Anclaje | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Autofactura | No existe esa capacidad en el dominio. Las únicas apariciones del concepto son la factura que Factuarea emite a sus propios suscriptores y la comprobación de la empresa del sistema — ninguna de las dos es una capacidad de la API. | | Factura expedida por un tercero | El campo AEAT de expedición por tercero no se emite nunca; cero apariciones en el código de la aplicación. | | Multidivisa | `InvoiceV1Resource` devuelve el literal `'EUR'`, y el repositorio de lectura de la v1 documenta que cualquier otra divisa produce una página vacía. | | TicketBAI / Batuz | `BR-VFC-019` — deliberadamente fuera del alcance del contexto VeriFactu. | | Inversión del sujeto pasivo por la API | Ninguna petición de la v1 acepta `operation_regime`; el recurso de factura lo devuelve de solo lectura. `BR-VFC-029` deriva la calificación de ese régimen de cabecera, y `BR-INV-032` acota el catálogo de línea a causas de exención y no sujeción, sin códigos `S`. | | Suplidos fuera de la factura emitida | `BR-INV-037` y el objeto de valor del tipo de línea, que declara que solo la factura emitida modela los suplidos; `BR-INV-040` para la restricción de la factura simplificada. | | Suplidos en el XML de Facturae y UBL | El edge case de aviso de `BR-INV-042`, que deja registrado que el importe a pagar de ambos documentos es el total fiscal y que el bloque nativo de Facturae 3.2.2 aún no está mapeado. | | Suplidos en las cifras agregadas de cartera | El edge case de `BR-INV-045`, que deja registrado que los agregados miden volumen facturado y que se dejan midiendo eso a propósito. | **Diferencias deliberadas:** ancladas a los componentes HTTP compartidos que implementan la alternativa —la paginación por cursor, el negociador de contenido de errores, el recurso de éxito parcial de las operaciones masivas—, a `BR-INV-044` para la suma de control opcional de línea, a `BR-VFC-003`, `BR-VFC-004`, `BR-VFC-022` y `BR-VFC-024` para los certificados propios de cada empresa, y a `BR-INV-015` y `BR-INV-016` para la sustitución en un solo paso. La fila del SDK de Python refleja una decisión registrada de planificarlo aparte cuando la especificación se estabilice: está aplazado, no descartado, y por eso su estado es *En roadmap* y no *Por diseño*. **Capacidades:** cada una se ancla a la ruta viva que la materializa — `public-api.v1.invoices.substitute_simplified`, `public-api.v1.verifactu.records.subsanar` y `public-api.v1.tax-catalog.show` — más `BR-VFC-033` para la base imponible negativa y `BR-TAX-028` para el catálogo fiscal. --- # Scopes e irreversibilidad (/es/guides/scopes-and-irreversibility) Cada endpoint público declara dos piezas de metadata de seguridad **en la especificación OpenAPI**: el scope exacto que enforza y si la operación se puede deshacer. Los clientes (el [CLI](/cli/agents), los agentes, tu propio tooling) las leen para fallar pronto — bloquear una llamada cuando la key carece del scope, confirmar antes de una acción irreversible — en lugar de descubrir el problema por un `403` o una mutación irrecuperable. ## Leerlo desde la especificación [#leerlo-desde-la-especificación] Cada operación en la [especificación OpenAPI](/api/openapi) lleva dos extensiones personalizadas: ```json { "operationId": "public-api.v1.invoices.delete", "x-required-scope": "invoices:delete", "x-irreversible": true } ``` * **`x-required-scope`** — el único scope `resource:action` que la API key debe tener para llamar a la operación. Presente en **todas** las operaciones. * **`x-irreversible`** — `true` solo en las operaciones que no se pueden deshacer. Ausente (tratado como `false`) en todo lo demás. <Callout type="info"> Son extensiones de proveedor `x-*`, así que un visor OpenAPI genérico puede no renderizarlas — pero cualquier cliente que parsee la especificación (como el CLI) las lee directamente. Genera un cliente desde la especificación y heredas ambas. </Callout> ## Scopes [#scopes] Los scopes son `resource:action` (p. ej. `invoices:read`, `clients:delete`) — el mismo catálogo cerrado que usan la REST API y las API keys. Una petición cuya key carece del scope devuelve `403` con `code: insufficient_scope`. Pide solo los scopes que tu integración necesita. Las operaciones de control horario llevan sus propios scopes — `employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read` y `payroll_exports:read` — todos tras el módulo `control_horario`. Consulta [Control horario](/guides/workforce-overview). El catálogo completo — cada scope, qué concede, y los sensibles — está en [Scopes y permisos](/mcp/scopes). Un scope **no** siempre se deriva del nombre del recurso: algunos endpoints enforzan un scope distinto del que adivinarías (las descargas de PDF enforzan `pdfs:read`, los métodos de pago enforzan `invoices:read`, las transiciones de estado enforzan un scope `:transition`, las acciones de VeriFactu enforzan `verifactu:*`). Lee siempre `x-required-scope` en lugar de inferirlo. <Callout type="info"> Una API key puede tener el super-scope `*`, que satisface cualquier `x-required-scope`. Consulta [Autenticación](/guides/authentication). </Callout> ## Operaciones irreversibles [#operaciones-irreversibles] Una operación marcada `x-irreversible: true` **no tiene deshacer**: borra datos, emite un registro fiscal, transiciona un documento a un estado terminal, o rota un secret. El [CLI](/cli/agents#irreversible-operations) pide una confirmación tipada antes de ejecutar una; tu propio tooling debería protegerlas igual. Estas son las categorías que llevan `x-irreversible: true`: | Categoría | Ejemplos | Scope | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | **Borrados** (individuales) | `clients.delete`, `invoices.delete`, `products.delete`, `taxes.delete`, `webhook_endpoints.delete`… | `<resource>:delete` | | **Borrados masivos** | `invoices.bulk_delete`, `clients.bulk_delete`, `products.bulk_delete`, `suppliers.bulk_delete`… | `<resource>:delete` | | **Emisión / numeración fiscal** | `invoices.send`, `invoices.mark_sent`, `invoices.assign_real_number`, `invoices.corrective`, `invoices.substitute_simplified` | `invoices:send` / `invoices:write` | | **Void / anulación** | `invoices.void`, `invoices.annul` | `invoices:void` | | **Conversiones terminales** | `quotes.convert`, `proformas.convert`, `delivery_notes.convert` | `<resource>:transition` | | **Cancelar / firmar** | `delivery_notes.cancel`, `delivery_notes.sign`, `recurring_invoices.cancel`, `recurring_invoices.generate` | `<resource>:transition` / `:write` | | **VeriFactu (AEAT)** | `invoices.verifactu_create`, `verifactu.records.subsanar`, `verifactu.settings.update`, `verifactu.certificates.revoke` | `verifactu:write` | | **Secret / certificado** | `webhook_endpoints.rotate_secret`, `verifactu.certificates.revoke` | `webhooks:write` / `verifactu:write` | | **Olvido GDPR** | `delivery_notes.signature_audits.forget` | `delivery_notes:gdpr_forget` | | **Envío FacturaE (B2G)** | `invoices.face_submissions.submit`, `face_submissions.cancel` | `facturae:write` | | **Sellado del cierre mensual** | `monthly_time_record_closes.seal` | `time_entries:write` | <Callout type="warn"> Esta lista es el resumen legible. La **fuente de verdad legible por máquina** es `x-irreversible` en la especificación — un cliente que la lee se mantiene correcto aunque el catálogo crezca. </Callout> ## Juntándolo todo [#juntándolo-todo] Un cliente seguro hace dos comprobaciones antes de una mutación: 1. **Scope-check** — ¿tiene la key el `x-required-scope`? Si no, para en local (sin gastar un round trip). El CLI sale con `4`; consulta [scope-check](/cli/agents#scope-check). 2. **Confirmación de irreversibilidad** — ¿es `x-irreversible` true? Si lo es, confirma antes de llamar. El CLI requiere `--confirm <id>`; consulta [operaciones irreversibles](/cli/agents#irreversible-operations). Los [SDKs](/sdks) oficiales y el [CLI](/cli) hacen ambas por ti. Si generas tu propio cliente desde la especificación, cablea estas dos comprobaciones tú mismo desde las extensiones. --- # Facturas simplificadas o completas (/es/guides/simplified-vs-full-invoices) La ley española distingue la **factura completa** (`F1`), que identifica al destinatario y le permite deducir el IVA, de la **factura simplificada** (`F2`), el tique que el comercio entrega en el mostrador. Cuando un cliente necesita después un documento deducible por un lote de tiques, la ley prevé un tercer tipo: la **factura sustitutiva** (`F3`), que agrega varias simplificadas. ## Cuándo aplica [#when] La factura simplificada está disponible en operaciones de tipo minorista por debajo de un importe legal. **Nunca** lo está en los casos de abajo, y la comprobación de admisibilidad los evalúa en este orden exacto — gana el primero que casa: | Condición que la bloquea | `reason_code` | | ------------------------------------------ | --------------------------- | | Operación intracomunitaria | `intra_community` | | Inversión del sujeto pasivo | `reverse_charge` | | Destinatario fuera de España (exportación) | `export_operation` | | El cliente necesita una factura deducible | `client_deduction_required` | | Total por encima del tope legal absoluto | `over_absolute_limit` | El tope que **el software sí aplica es de 3.000 € con IVA incluido** — el máximo que puede alcanzar cualquier factura simplificada según el RD 1619/2012 art. 4, sea cual sea el sector. Superarlo responde `422` con el código de motivo `over_absolute_limit` ([`BR-INV-009`](#traceability)). <Callout type="warn"> El umbral general de 400 € del mismo artículo **no** se aplica. Factuarea no pide a la empresa que declare su sector económico, así que no puede saber si le corresponde el límite elevado. Mantenerse por debajo de 400 € cuando tu sector no da derecho a la cifra mayor es responsabilidad fiscal del emisor, no algo que la API vaya a impedirte. </Callout> El catálogo de impuestos no cambia entre los dos tipos. A una `F1` y a una `F2` les aplican los mismos tipos de IVA; lo que difiere es el contenido obligatorio del documento, el tope de importe y la identificación del destinatario ([`BR-TAX-011`](#traceability)). ## Qué envía la API [#api] ### Pregunta antes de decidir [#eligibility] [`POST /v1/invoices/simplified-eligibility`](/api-reference/invoices/public-api.v1.invoices.simplified_eligibility), scope `invoices:read`. Pensado para flujos de caja y de punto de venta que deben elegir el tipo de documento *antes* de crear nada. ```bash curl -X POST https://api.factuarea.com/v1/invoices/simplified-eligibility \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"total": 3400, "client_country": "ES"}' ``` ```json { "data": { "can_be_simplified": false, "must_be_complete": true, "reason_code": "over_absolute_limit", "reason_message": "El importe 3.400,00 EUR supera el límite de 3.000,00 EUR para una factura simplificada.", "sector_limit": 3000 } } ``` `total` es obligatorio y es el importe **con IVA incluido**. `client_id`, `client_country`, `is_intra_community`, `is_reverse_charge` y `client_requires_deductible` son entradas opcionales a las condiciones de bloqueo de arriba. ### Crear una factura simplificada no es una operación de la v1 [#f2-not-in-v1] [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) no tiene campo `type` ni bandera de simplificada, y `client_id` es obligatorio. **La API pública no puede emitir una `F2`.** Toda factura creada por la v1 es una factura completa. Es una frontera real, no una omisión que puedas sortear con un truco de payload. Si tu punto de venta emite facturas simplificadas, se crean por el panel o por la superficie de punto de venta; lo que la v1 te da sobre ellas es la comprobación de admisibilidad, la lectura y la sustitución de abajo. La consecuencia para el tratamiento de errores: el `422` por una línea de suplido dentro de una factura simplificada es inalcanzable desde `POST /v1/invoices` y solo se alcanza por el endpoint de rectificativa sobre un original simplificado — ver [Suplidos](/guides/disbursements). ### Sustituir facturas simplificadas, en una sola llamada [#substitute] [`POST /v1/invoices/substitute-simplified`](/api-reference/invoices/public-api.v1.invoices.substitute_simplified), scope `invoices:write`. Pasas el destinatario y la lista de facturas simplificadas que quieres agregar; recibes una `F3` completa, ya emitida, con número definitivo de serie: ```bash curl -X POST https://api.factuarea.com/v1/invoices/substitute-simplified \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "simplified_invoice_ids": [ "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f601", "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f602" ], "notes": "Consumos de junio" }' ``` Cada factura simplificada de la lista se valida ([`BR-INV-015`](#traceability)): la lista no puede estar vacía ni tener duplicados, cada factura debe pertenecer a tu empresa, cada una debe ser realmente una `F2`, ninguna puede estar anulada ni cancelada, y ninguna puede tener ya una sustitutiva. Cuando falla, el `422` nombra el número de la factura que lo provoca. La `F3` nace emitida, y sus líneas son agregados: una línea por factura sustituida, descrita como la sustitución de ese número de factura, con cantidad uno y el total bruto original como precio unitario, y **sin impuesto propio** — el IVA ya se repercutió en la factura simplificada. <Callout type="info"> La sustitución no anula los originales. Cada `F2` conserva su estado fiscal y simplemente deja constancia de que ha sido sustituida; el objeto factura lo expone como `substituted_by`. Una `F3` **sí** se puede rectificar, como cualquier factura completa —los códigos `R1`–`R4` valen igual para `F1` que para `F3`, ver [Facturas rectificativas](/guides/corrective-invoices)—. Lo que una `F3` no puede ser es *sustituida*: solo una `F2` puede ser objeto de una sustitución, y solo una `F3` puede llevar facturas sustituidas ([`BR-INV-016`](#traceability)). </Callout> ## Qué sale en el PDF [#pdf] La diferencia visible es el bloque de destinatario. Una factura completa imprime el nombre, el NIF y la dirección del destinatario, congelados en el momento de emitir; una simplificada puede legítimamente no tener ninguno, e imprime en su lugar el marcador de consumidor final ([`BR-INV-024`](#traceability)). La `F3` se imprime como una factura completa ordinaria — un bloque de destinatario completo y una línea por cada tique sustituido, nombrando cada número de factura sustituida. ## Qué llega a la AEAT [#aeat] El tipo de factura viaja como el `TipoFactura` de la AEAT en el registro VeriFactu y es visible en el objeto registro como `invoice_type`: `F1`, `F2`, `F3`, o `R5` para una rectificativa de una simplificada. El registro señala además si sustituye facturas simplificadas ([`BR-VFC-014`](#traceability)). El tipo tiene también consecuencias en las declaraciones periódicas ([`BR-TXR-004`](#traceability)): * Una `F3` sin NIF de destinatario levanta un aviso **no bloqueante** en la declaración trimestral de IVA: es inusual, pero legítimo si el tique original tampoco lo tenía. * Una `F2` con cliente registrado pero sin NIF levanta también un aviso. * Una `F2` sin NIF alguno queda **excluida de la declaración anual de operaciones con terceras personas** (**Modelo 347**) por norma de la AEAT, y la exclusión se informa como aviso. Ninguno de ellos bloquea la generación. El informe se produce y los avisos se devuelven junto a él, como lista vacía cuando no hay ninguno. Bloquear sería un falso positivo frecuente. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-INV-009` — el tope aplicado de 3.000 €, el umbral no aplicado de 400 € y las operaciones que descartan la factura simplificada. * `BR-INV-015` — sustitución de facturas simplificadas por una `F3`, sus validaciones y sus líneas agregadas. * `BR-INV-016` — solo una `F3` lleva facturas sustituidas, y nunca una lista vacía. * `BR-INV-024` — el snapshot inmutable del destinatario, y su ausencia en un tique de consumidor final. * `BR-TAX-011` — el catálogo de impuestos es idéntico para ambos tipos; el límite pertenece a la facturación, no al catálogo. * `BR-VFC-014` — los tipos de factura de la AEAT y cómo se resuelve el tipo. * `BR-TXR-004` — avisos no bloqueantes de calidad fiscal para `F2` y `F3` sin NIF. Los códigos de motivo del `422` citados en [Cuándo aplica](#when) vienen del servicio de dominio de admisibilidad que materializa `BR-INV-009`. --- # Etiquetas y campos personalizados (/es/guides/tags-and-custom-fields) Dos campos transversales te permiten clasificar y enriquecer los documentos con tus propios datos de negocio: **`tags`** (etiquetas de clasificación libres por las que puedes filtrar los listados) y **`custom_fields`** (una lista ordenada de pares tipados `{field, value}`). Ambos se establecen en create/update y se devuelven en cada lectura. | Campo | Forma | Límites | Filtrable | Recursos | | --------------- | ---------------------------------- | ------------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------- | | `tags` | array de slugs | ≤ 30, cada uno ≤ 40 caracteres | **Sí** (`?tags=`, `?tags[in]=`) | invoices, quotes, proformas, delivery\_notes, purchase\_invoices, recurring\_invoices, products | | `custom_fields` | array ordenado de `{field, value}` | ≤ 50 entradas | No | invoices, quotes, proformas, delivery\_notes, purchase\_invoices, recurring\_invoices | ## Etiquetas [#etiquetas] Un tag es un **slug en minúscula** que cumple `[a-z0-9-]` — solo letras, dígitos y guiones. Cada tag mide como máximo **40 caracteres**, y un documento lleva como máximo **30 tags**. Pásalos como un array JSON de strings al crear o actualizar: ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "tags": ["consultoria", "cliente-vip"], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` Los tags se devuelven como un array plano en cada lectura (vacío `[]` cuando no hay ninguno): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "number": "F-2026-0042", "tags": ["consultoria", "cliente-vip"] } ``` <Callout type="warn"> `tags` es un **reemplazo completo** al actualizar: enviar `"tags": ["a"]` sustituye todo el conjunto, no lo añade. Para agregar un tag, envía la lista completa incluidos los existentes. Para vaciarlos, envía `[]`. </Callout> ### Filtrar por tag [#filtrar-por-tag] Los endpoints de listado aceptan dos parámetros de query para filtrar por tag — elige uno: | Parámetro | Semántica | Ejemplo | | ---------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `tags` | **Coincidencia exacta** con un único slug. | `?tags=cliente-vip` | | `tags[in]` | Lista separada por comas, semántica **OR** — coincide con los documentos que lleven **cualquiera** de los slugs. | `?tags[in]=cliente-vip,moroso` | ```bash # Todas las facturas etiquetadas con "cliente-vip" curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags=cliente-vip" # Facturas etiquetadas con "cliente-vip" O "moroso" curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags[in]=cliente-vip,moroso" ``` El filtro por tag está disponible en `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices` y `recurring_invoices`. Se combina con los demás filtros y con la [paginación por cursor](/es/guides/pagination). ## Campos personalizados [#campos-personalizados] `custom_fields` es un **array ordenado** de pares tipados `{field, value}`, para datos de negocio que quieras mostrar junto al documento (centro de coste, número de pedido, código de proyecto…). Hasta **50** entradas; cada `field` es un string no vacío de como máximo **60 caracteres** y cada `value` es un string de como máximo **500 caracteres**. El orden se conserva exactamente como lo envías. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "custom_fields": [ { "field": "centro_coste", "value": "CC-2026-001" }, { "field": "numero_pedido", "value": "PO-2026-0042" } ], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` Igual que `tags`, el array completo es un reemplazo total al actualizar, y se devuelve en orden en cada lectura (vacío `[]` cuando no hay ninguno). ## Campos personalizados frente a metadata [#campos-personalizados-frente-a-metadata] Tanto `custom_fields` como `metadata` llevan datos tuyos, pero sirven para propósitos distintos — no uses el que no toca. <Callout type="info"> Usa **`custom_fields`** para datos de negocio ordenados y tipados que el usuario ve en el documento. Usa **`metadata`** para un mapa desordenado clave→valor de datos de integración opacos (códigos de ERP, referencias de tu propia contabilidad) que nadie lee visualmente. Un documento puede llevar **ambos**. </Callout> | | `custom_fields` | `metadata` | | --------- | -------------------------------------- | ---------------------------------- | | Forma | **Lista** ordenada de `{field, value}` | **Mapa** desordenado `key → value` | | Orden | Se conserva | Ninguno | | Límite | ≤ 50 entradas | ≤ 50 claves | | Clave | `field`, 1–60 caracteres | clave del mapa | | Valor | string ≤ 500 caracteres | string ≤ 500 caracteres | | Intención | Campos de negocio que el usuario ve | Datos de integración opacos | | Recursos | Los seis recursos de documento | Todos los recursos | Los recursos maestros (`clients`, `suppliers`) no tienen `custom_fields` tipados — usa su `metadata` como almacén de campos personalizados sin tipar. Los `products` aceptan `tags` pero no `custom_fields`. ## Ejemplos [#ejemplos] <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests base = 'https://api.factuarea.com/v1' headers = {'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"} # Create an invoice with tags + custom_fields resp = requests.post(f'{base}/invoices', headers=headers, json={ 'client_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01', 'series_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02', 'issued_on': '2026-05-15', 'due_on': '2026-06-15', 'tags': ['consultoria', 'cliente-vip'], 'custom_fields': [ {'field': 'centro_coste', 'value': 'CC-2026-001'}, {'field': 'numero_pedido', 'value': 'PO-2026-0042'}, ], 'lines': [ {'description': 'Monthly service', 'quantity': 1, 'unit_price': 99.00, 'tax_rate_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03'}, ], }) resp.raise_for_status() # List invoices tagged "cliente-vip" OR "moroso" rows = requests.get(f'{base}/invoices', headers=headers, params={'tags[in]': 'cliente-vip,moroso'}).json()['data'] print(len(rows), 'matching invoices') ``` </Tab> <Tab value="Node.js"> ```javascript const base = 'https://api.factuarea.com/v1'; const headers = { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }; // Create an invoice with tags + custom_fields await fetch(`${base}/invoices`, { method: 'POST', headers, body: JSON.stringify({ client_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01', series_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02', issued_on: '2026-05-15', due_on: '2026-06-15', tags: ['consultoria', 'cliente-vip'], custom_fields: [ { field: 'centro_coste', value: 'CC-2026-001' }, { field: 'numero_pedido', value: 'PO-2026-0042' }, ], lines: [ { description: 'Monthly service', quantity: 1, unit_price: 99.0, tax_rate_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03' }, ], }), }); // List invoices tagged "cliente-vip" OR "moroso" const url = new URL(`${base}/invoices`); url.searchParams.set('tags[in]', 'cliente-vip,moroso'); const { data } = await fetch(url, { headers }).then((r) => r.json()); console.log(data.length, 'matching invoices'); ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash # Create an invoice with tags + custom_fields curl -s -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "tags": ["consultoria", "cliente-vip"], "custom_fields": [ { "field": "centro_coste", "value": "CC-2026-001" }, { "field": "numero_pedido", "value": "PO-2026-0042" } ], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' | jq '{id, tags, custom_fields}' # List invoices tagged "cliente-vip" OR "moroso" curl -s -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags[in]=cliente-vip,moroso" | jq '.data | length' ``` </Tab> </Tabs> --- # Impuestos territoriales — IVA, IGIC e IPSI (/es/guides/territorial-taxes) La península y Baleares repercuten **IVA**. Canarias repercute **IGIC**. Ceuta y Melilla repercuten **IPSI**. Son tres impuestos distintos con tres rejillas de tipos distintas, tres códigos AEAT distintos y tres administraciones distintas — y tratarlos como uno solo es la vía por la que una empresa canaria acaba sobredeclarando su IVA. ## Cuándo aplica [#when] El régimen aplicable se deriva de la zona AEAT de la empresa emisora: península → IVA, Canarias → IGIC, Ceuta y Melilla → IPSI ([`BR-TAX-020`](#traceability), art. 1.3 RRSIF). La derivación es el **valor por defecto**, no toda la historia. Como una operación puede localizarse en un sitio distinto de donde está establecido el emisor, un documento puede sobrescribir el régimen de forma explícita ([`BR-TAX-027`](#traceability)) — ver [Elegir el régimen](#override). ## Las tres rejillas de tipos [#rates] Cada régimen tiene una rejilla cerrada de tipos legales y un tipo general ([`BR-TAX-020`](#traceability)): | Régimen | Código AEAT | Tipos legales | Tipo general | | ------- | ----------- | ----------------------- | ------------ | | IVA | `01` | 0, 4, 10, 21 | 21 % | | IPSI | `02` | 0, 0,5, 1, 2, 4, 8, 10 | 8 % | | IGIC | `03` | 0, 3, 5, 7, 9,5, 15, 20 | 7 % | El 0 % es válido en los tres — representa la operación exenta. <Callout type="warn"> Fíjate en los códigos AEAT: **el IPSI es el `02` y el IGIC el `03`**, no al revés. Un objeto de valor interno anterior los tenía invertidos; emitir el equivocado produce un rechazo o una declaración errónea. </Callout> La rejilla **se impone al crear o editar un impuesto de IGIC o de IPSI**: un tipo fuera de la rejilla de su régimen responde `422` listando los tipos legales ([`BR-TAX-021`](#traceability)). Mover un impuesto existente a otra zona revalida su tipo contra el régimen nuevo, así que un impuesto al 21 % no puede reetiquetarse como canario sin cambiar antes el tipo. Al IVA **no** se le estrecha así a propósito. El catálogo sembrado contiene tipos históricos y transitorios —2 %, 5 %, 7,5 % de las medidas antiinflación— que no pertenecen a una rejilla cerrada, y rechazarlos rompería los datos existentes. ## Qué envía la API [#api] ### Encontrar los impuestos correctos [#catalog] [`GET /v1/taxes`](/api-reference/taxes/public-api.v1.taxes.list) acepta tanto `country_aeat_zone` (`peninsula`, `canarias`, `ceuta`, `melilla`) como el derivado `indirect_tax_regime` (`iva`, `igic`, `ipsi`). Son dos vistas equivalentes de la misma dimensión: `?indirect_tax_regime=igic` equivale a `?country_aeat_zone=canarias` ([`BR-TAX-026`](#traceability)). ```bash curl "https://api.factuarea.com/v1/taxes?indirect_tax_regime=igic" \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Cada impuesto expone su `indirect_tax_regime`, derivado en solo lectura de su zona, y `linked_surcharge_taxes_id`, el recargo de equivalencia legalmente emparejado con él. Un régimen desconocido en el filtro degenera en una lista vacía — nunca inventa resultados. Los impuestos que no gravan el consumo —retenciones, recargos— llevan un régimen nulo. ### Elegir el régimen de un documento [#override] `lines[].indirect_tax_regime` acepta `iva`, `igic` o `ipsi` en [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) y [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). La precedencia es **sobrescritura sobre zona** ([`BR-TAX-027`](#traceability)): <Steps> <Step> Una sobrescritura válida **gana** al régimen derivado de la zona del impuesto. </Step> <Step> Sin sobrescritura → el régimen se deriva de la zona, el comportamiento histórico. </Step> <Step> Ni impuesto resoluble ni sobrescritura → el snapshot se queda vacío. No se infiere nada. </Step> </Steps> Aplican dos guardas, ambas con `422`: * Un valor fuera de `iva|igic|ipsi` se rechaza como invariante de **dominio**, no solo de formulario — la misma respuesta venga de la superficie que venga. * **Todas las líneas de un documento deben compartir el mismo régimen.** La sobrescritura es de documento, no de línea; mezclar dos regímenes responde `422` con una violación de regla de negocio. Las líneas sin régimen las ignora la comprobación, así que un documento que combine líneas `igic` explícitas con líneas sin tipar es homogéneo. ```json { "lines": [ { "description": "Servicio prestado en Canarias", "quantity": 1, "unit_price": 100, "indirect_tax_regime": "igic" }, { "description": "Materiales", "quantity": 2, "unit_price": 50, "indirect_tax_regime": "igic" } ] } ``` El régimen elegido forma parte del **snapshot fiscal inmutable** de la línea, así que **sobrevive a la conversión**: un presupuesto o una proforma convertidos en factura heredan el régimen que se eligió, en lugar de recalcularlo con la zona que tenga hoy la empresa ([`BR-TAX-023`](#traceability)). El campo hermano `aeat_tax_code` se deriva siempre del impuesto y no es sobrescribible nunca; si llega un valor, se ignora. ## Qué sale en el PDF [#pdf] La columna de impuesto y el bloque de totales muestran los tipos que se aplicaron de verdad, así que una factura con IGIC imprime tipos de IGIC. El nombre del régimen no es un elemento impreso aparte; se ve a través de los tipos y, cuando el documento la lleva, de la mención legal de su causa de exención ([`BR-TAX-024`](#traceability)). Como el snapshot es inmutable, una empresa que traslade después su domicilio fiscal no cambia retroactivamente los documentos que ya emitió. ## Qué llega a la AEAT [#aeat] **En el registro VeriFactu**, el campo `Impuesto` de cada grupo de desglose se deriva del régimen de la línea —`01` para IVA, `02` para IPSI, `03` para IGIC— y no se fija a fuego nunca ([`BR-VFC-034`](#traceability)). Una factura mixta produce un grupo de desglose por cada par (tipo, régimen), y la huella sella el conjunto. Las líneas históricas sin snapshot de régimen conservan `01`, de modo que no se altera el XML de facturas ya declaradas. La clave de régimen viaja de otra manera: bajo IPSI **no se emite en absoluto**, y bajo IVA e IGIC se deriva en lugar de fijarse a fuego — ver [Claves de régimen](/guides/regime-keys#aeat). **En la declaración trimestral de IVA**, la regla es absoluta: [`POST /v1/tax_reports/303`](/api-reference/tax-reports/public-api.v1.tax_reports.generate_303) agrega **solo** las líneas cuyo régimen sea IVA ([`BR-TXR-039`](#traceability), [`BR-TXR-020`](#traceability)): * **IVA repercutido.** Una línea de cualquier otro régimen se salta. No llega nunca a una casilla de IVA. * **IVA soportado.** La base y la cuota de las líneas que no son de IVA se restan del total de la factura, de modo que una compra puramente de IVA conserva exactamente su importe anterior, y una compra mixta aporta solo su parte de IVA. El IGIC y el IPSI soportados **no son deducibles** en esta declaración — son impuestos distintos. El IGIC se liquida ante la Agencia Tributaria Canaria; el IPSI, ante la administración local de Ceuta o Melilla. Ninguno de los dos tiene nada que ver con la declaración estatal de IVA. Las líneas del snapshot se agrupan por la clave compuesta **(régimen, tipo)** y no solo por el tipo, que es lo que impide que una línea de IPSI al 10 % se fusione con una línea de IVA al 10 % ([`BR-TXR-038`](#traceability)). Entre las líneas de IVA se emite todo tipo presente —incluidos el 2 %, el 5 % y el 7,5 %—, así que no se pierde nada del fichero de la AEAT por caer fuera de los tres habituales. ### El aviso territorial es un aviso, nunca un bloqueo [#warning] Una empresa de territorio especial que genere su declaración de IVA recibe un **aviso en español** en la lista `warnings`, nombrando la administración ante la que se liquida el impuesto indirecto. El fichero se produce igualmente, solo con la parte de IVA ([`BR-TXR-040`](#traceability)). No es un `422` a propósito. Una empresa canaria puede tener IVA perfectamente legítimo —ventas a la península, por ejemplo— y bloquear le negaría una declaración válida. El aviso aparece solo cuando la zona es especial **y** el periodo contiene realmente operaciones de impuesto indirecto, y se acumula con los demás avisos de calidad fiscal. La declaración anual de operaciones con terceras personas (**Modelo 347**) se comporta de otra manera: **sí** incluye las operaciones de IGIC y de IPSI por su importe total con el impuesto incluido, porque es agnóstica a qué impuesto indirecto aplica. Ver [Suplidos](/guides/disbursements#aeat) para lo que cambia su base. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-TAX-020` — el régimen indirecto como objeto de valor derivado de la zona AEAT, sus rejillas de tipos legales y sus códigos AEAT. * `BR-TAX-021` — la imposición de la rejilla de tipos al crear o editar un impuesto de IGIC o de IPSI, y por qué al IVA no se le estrecha. * `BR-TAX-023` — el snapshot fiscal inmutable por línea. * `BR-TAX-024` — la causa de exención a nivel de documento y su mención legal. * `BR-TAX-026` — el filtrado del catálogo por zona y por régimen, y el recargo vinculado que se expone. * `BR-TAX-027` — la sobrescritura de régimen por documento, su precedencia, sus dos guardas con `422` y su supervivencia a la conversión. * `BR-VFC-034` — el `Impuesto` del desglose derivado por línea, nunca fijado a fuego. * `BR-TXR-020` — la declaración de IVA agrega solo líneas de IVA, y no pierde ningún tipo de IVA. * `BR-TXR-038` — el snapshot agrupa por (régimen, tipo). * `BR-TXR-039` — exclusión del IGIC y del IPSI tanto del IVA repercutido como del soportado. * `BR-TXR-040` — el aviso territorial que nunca bloquea la generación. --- # Modo de prueba y sandbox (/es/guides/test-mode) Cada API key de Factuarea pertenece a uno de dos **entornos**, distinguidos por su prefijo: | Prefijo | Entorno | Opera sobre | Efectos externos | | ------------ | --------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------- | | `fact_live_` | **live** (producción) | Tu empresa real | Reales: numeración fiscal legal, VeriFactu → AEAT, envíos a FACe, emails a clientes, webhooks salientes | | `fact_test_` | **test** (sandbox) | Una empresa *sandbox* aislada | **Desactivados** (ver abajo) | El prefijo es la **fuente de verdad** del entorno: una clave `fact_test_` siempre opera en modo de prueba y una clave `fact_live_` siempre en producción. Ningún parámetro de la petición cambia el entorno — lo determina por completo la clave con la que te autenticas. <Callout type="info"> Crea y prueba siempre tu integración primero con una clave `fact_test_`. Una vez tu flujo funcione de extremo a extremo, cambia el prefijo a `fact_live_` para pasar a producción. La superficie de la API es **idéntica** en ambos entornos. </Callout> ## Obtener una clave de prueba [#obtener-una-clave-de-prueba] Las claves de prueba se crean desde el dashboard de desarrolladores exactamente igual que las claves live, seleccionando el entorno **Test** ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)). El secreto generado tiene este aspecto: ``` fact_test_<24 alphanumeric characters> ``` Ejemplo: ``` fact_test_3pXnR2VbY7TcA9eFmN5z8KqW ``` Mismo formato y entropía que una clave live (24 caracteres base62), mismos scopes, mismo nivel de rate limit. La única diferencia es el prefijo y aquello a lo que apunta. Igual que con las claves live, el secreto se muestra **solo una vez** al crearlo — si lo pierdes, tendrás que rotarlo. ## Usar una clave de prueba [#usar-una-clave-de-prueba] Envíala en cada petición igual que una clave live, mediante `Authorization: Bearer` o `X-API-Key`: ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Los mismos endpoints y operaciones disponibles en live lo están en test — no se elimina ni se simula nada. ## Aislamiento de datos: la empresa sandbox [#aislamiento-de-datos-la-empresa-sandbox] Una clave `fact_test_` opera sobre una **empresa sandbox** dedicada — un "gemelo" técnico de tu empresa real, aprovisionado automáticamente la primera vez que usas el modo de prueba, que hereda el plan de tu empresa real para que la limitación por funcionalidades sea fiel. Gracias al aislamiento multi-tenant por empresa: * Los recursos creados con una clave `fact_test_` **no son visibles** para una clave `fact_live_`, y viceversa. * La numeración fiscal de prueba usa las series propias del sandbox y **nunca** consume ni altera la numeración correlativa de tus series de producción. Esto es aislamiento estructural, no un filtro: los datos de test y live viven en empresas separadas, así que no hay forma de que se mezclen. ## Qué está desactivado en test [#qué-está-desactivado-en-test] Cuando operas con una clave `fact_test_` (entorno sandbox), los efectos que alcanzan al mundo exterior están **deshabilitados** para que puedas ejercitar tu integración sin consecuencias en el mundo real: | Efecto | En `live` | En `test` | | ------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **VeriFactu** | El registro de Alta se crea y se transmite a la AEAT. | El registro de Alta se crea **localmente**, pero **nunca se transmite a la AEAT**. | | **Email** | Los emails de documentos se entregan a destinatarios reales. | Los emails de documentos **no se entregan** a destinatarios reales. | | **Webhooks** | Los eventos suscritos se entregan a tus endpoints HTTP externos. | Los eventos se registran con `livemode: false` (consultables vía `GET /v1/events`) pero **no se entregan** a tus endpoints. | | **FACe (FacturaE)** | Los envíos se presentan al web service real de FACe. | Todo el flujo se **simula** — ninguna llamada SOAP sale de Factuarea y el número de registro es sintético (`FACE-SANDBOX-*`). Consulta [Facturación FACe](/guides/face-invoicing#sandbox). | Todo lo demás se comporta de forma idéntica: validación, totales, máquinas de estado de documentos, idempotencia, paginación, rate limits y envoltorios de error son los mismos que en producción. <Callout type="warn"> Como los webhooks no se entregan en test, no puedes ejercitar tu receptor de webhooks contra datos del sandbox. Prueba la verificación de firma de tu endpoint con el `POST /v1/webhook_endpoints/{id}/ping` dedicado (que sí se entrega) o contra una clave live en un evento controlado. </Callout> ## Con los SDK oficiales [#con-los-sdk-oficiales] Los [SDK de TypeScript y PHP](/sdks) siguen la misma regla: **el prefijo de la clave selecciona el entorno** — no hay ningún flag. Crea tu integración con una clave `fact_test_` y luego cambia la variable de entorno a `fact_live_` para pasar a producción. Sin cambios de código. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const sandbox = new Factuarea({ apiKey: "fact_test_…" }); sandbox.environment; // "test" const prod = new Factuarea({ apiKey: "fact_live_…" }); prod.environment; // "live" ``` El SDK expone el entorno resuelto en `.environment`, derivado del prefijo — útil para guardas y logging. </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\FactuareaClient; $sandbox = FactuareaClient::create('fact_test_…'); // sandbox $prod = FactuareaClient::create('fact_live_…'); // production ``` </Tab> </Tabs> <Callout type="warn"> Los webhooks siguen **sin entregarse** en test, incluso a través del SDK. Para ejercitar el [verificador del SDK](/sdks#verifying-webhooks) de tu receptor en el sandbox, usa `POST /v1/webhook_endpoints/{id}/ping`, que *sí* se entrega. </Callout> ## Cambiar de entorno en la app [#cambiar-de-entorno-en-la-app] Más allá de las API keys, la app web de Factuarea te permite alternar entre **live** y **test** en cualquier momento desde la barra superior. Al cambiar a test: * Se reemite tu sesión **sin volver a iniciar sesión**, apuntándola a la empresa sandbox (aprovisionándola si aún no existe). El token anterior se invalida. * Se muestra un banner persistente **"MODO TEST"** en toda la interfaz para que el contexto activo sea siempre evidente. * Se rehidrata el estado del cliente para que los listados reflejen los datos del sandbox y nunca muestren datos de producción cacheados. Volver a live reemite la sesión contra tu empresa real. El sandbox nunca se muestra como una empresa real en el selector de empresas — existe únicamente para dar soporte al entorno de prueba. ## De test a producción [#de-test-a-producción] Cuando tu integración funcione contra `fact_test_`: 1. Crea una clave `fact_live_` en el dashboard (los mismos scopes que validaste en test). 2. Cambia la clave que usa tu cliente (variable de entorno / gestor de secretos). 3. No hace falta ningún cambio de código — la forma de la petición es idéntica. A partir de ese momento, los efectos reales (numeración fiscal, VeriFactu → AEAT, emails, webhooks) vuelven a estar activos. --- # Fichajes (/es/guides/time-clock) Fichar escribe en un **ledger de solo apéndice**: fichar entrada, pausar, reanudar y fichar salida apéndican cada uno una entrada nueva que jamás se edita ni se borra. Cada entrada se encadena a la anterior con una **huella SHA-256** ([cadena por empresa](/guides/workforce-overview)), de modo que cualquier manipulación es detectable. El **estado de la jornada en vivo** —`not_started`, `working`, `paused`, `finished`— se **deriva** del ledger, no se guarda en una columna. Todos los endpoints viven bajo `https://api.factuarea.com/v1` y usan los scopes `time_entries:read` / `time_entries:write`, el mismo [envoltorio de error](/guides/errors) y [paginación por cursor](/guides/pagination) que el resto de la API. ## Fichar entrada, pausar, reanudar, salir [#clocking] Cuatro operaciones de escritura gobiernan la jornada. Cada una acepta un `occurred_at` opcional (por defecto, ahora) y un `source`, y devuelve la entrada apéndicada. | Operación | Endpoint | Desde estado | | ----------------- | --------------------------------- | -------------------------- | | Fichar entrada | `POST /v1/time-entries/clock-in` | `not_started` o `finished` | | Iniciar una pausa | `POST /v1/time-entries/pause` | `working` | | Reanudar | `POST /v1/time-entries/resume` | `paused` | | Fichar salida | `POST /v1/time-entries/clock-out` | `working` o `paused` | Dos reglas rigen la secuencia. **Una jornada abierta a la vez**: fichar entrada dos veces devuelve `422` ("Ya has fichado la entrada."); pausar o fichar salida sin jornada abierta devuelve `422`. **Cronología monótona**: un `occurred_at` anterior al último evento de la franja se rechaza con `422`. Una jornada puede tener **varias franjas** (jornada partida) — volver a fichar entrada tras la salida abre una franja nueva. ```bash curl -X POST https://api.factuarea.com/v1/time-entries/clock-in \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "web" }' ``` Lee la sesión abierta actual con `GET /v1/time-entries/current`, lista entradas con `GET /v1/time-entries` y obtén una con `GET /v1/time-entries/{time_entry}` — todo bajo `time_entries:read`. Consulta los esquemas en la [Referencia de API](/api-reference/time-entries/public-api.v1.time_entries.clock_in). ## Fichajes retroactivos (manuales) [#manual] `POST /v1/time-entries/manual` registra una **franja pasada completa** (entrada, pausas opcionales y salida) para un empleado que olvidó fichar. El `reason` es **obligatorio** — se sella en la cadena de huellas como parte de la evidencia — y la entrada se marca `is_retroactive` con `source: manual`. A diferencia del fichaje live self-service, un fichaje manual es una acción privilegiada y se registra en el audit log. ```bash curl -X POST https://api.factuarea.com/v1/time-entries/manual \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "started_at": "2026-02-03T09:00:00+01:00", "ended_at": "2026-02-03T17:00:00+01:00", "reason": "Olvidó fichar la entrada; confirmado por el responsable" }' ``` ## El flujo de correcciones [#corrections] Un registro de jornada **nunca** se edita. Para enmendar un error, un empleado abre una **solicitud de corrección**; un manager o admin la aprueba o la rechaza. Una solicitud pasa `pending → approved` o `pending → rejected`, ambos terminales. | Operación | Endpoint | Efecto | | ------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Solicitar una corrección | `POST /v1/time-corrections` | Crea una solicitud `pending`. | | Aprobar | `POST /v1/time-corrections/{correction}/approve` | Apéndica una entrada de corrección al original; emite `time_entry.corrected`. | | Rechazar | `POST /v1/time-corrections/{correction}/reject` | Registra un rechazo con motivo; el original queda intacto. | | Listar / detalle | `GET /v1/time-corrections`, `GET /v1/time-corrections/{correction}` | Lee el estado del flujo. | La aprobación **apéndica una entrada nueva** que referencia a la original — el error y su enmienda quedan ambos en el ledger. Dos guardas aplican: **no puedes aprobar tu propia** solicitud (`422`), y una solicitud ya resuelta no se resuelve de nuevo (`422`). ```bash curl -X POST https://api.factuarea.com/v1/time-corrections/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/approve \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "note": "Verificado contra el registro de accesos" }' ``` <Callout type="warn"> No hay actualización ni borrado para un registro de jornada. Cada corrección es una **entrada nueva** que mantiene el original intacto — eso es lo que hace el registro defendible ante la Inspección de Trabajo. </Callout> ## Verificar la integridad de la cadena [#chain] `GET /v1/time-entries/chain/validate` recalcula toda la cadena de huellas e informa de si está intacta, devolviendo el id de la primera entrada rota si la hay. Es una comprobación de integridad de **solo lectura** (con límite de tasa) — úsala para demostrar que el registro no ha sido alterado. ## Próximos pasos [#next] * [Horarios](/guides/work-schedules) — las horas esperadas contra las que se mide el ledger. * [Cierre mensual](/guides/monthly-time-close) — congelar y sellar un mes finalizado. * Explora la referencia de [fichajes](/api-reference/time-entries/public-api.v1.time_entries.list) y [correcciones](/api-reference/time-corrections/public-api.v1.time_corrections.create). --- # Alta automática en VeriFactu (/es/guides/verifactu-auto-submission) Quien integra viniendo de otras plataformas de facturación busca la operación que envía una factura a la Administración tributaria, no la encuentra y da por hecho que la funcionalidad falta. No falta: **el alta no es un paso que ejecutes tú.** El registro se crea como consecuencia de emitir la factura, y lo transmite una tubería en segundo plano. Esta página responde a «¿por qué mi factura no ha llegado a la AEAT?», que casi siempre es una de las compuertas de abajo y no un fallo. ## Cuándo aplica [#when] A toda factura que sale de `draft` en una empresa cuya activación de VeriFactu es efectiva. En concreto, el alta se crea en la transición a `sent` — incluidas las facturas que nacen ya emitidas: rectificativas, sustitutivas `F3`, generaciones de recurrentes y creaciones que pasan `status: sent` directamente. La etapa `draft` queda deliberadamente fuera del mecanismo. Un borrador no tiene número definitivo, ni snapshot congelado del destinatario, ni existencia fiscal; no se declara nada por él. ## Las compuertas, en el orden en que se evalúan [#gates] <Steps> <Step> **Interruptor de emergencia de la instancia.** Una bandera global puede desactivar VeriFactu para toda la instalación. Es un interruptor de emergencia, nunca una activación: por sí sola no habilita nada. </Step> <Step> **Activación por empresa.** Esta es la que controlas tú. Viene **desactivada** de fábrica en una cuenta recién creada — una empresa nueva *no* da de alta sus facturas hasta que alguien activa VeriFactu. La activación efectiva es `instancia Y empresa` ([`BR-VFC-025`](#traceability)). Léela con [`GET /v1/verifactu/config`](/api-reference/verifactu/public-api.v1.verifactu.config): el campo `enabled` ya es el valor efectivo, no la bandera cruda de la empresa. </Step> <Step> **Modo de funcionamiento.** Con la activación puesta, la empresa aún elige entre transmitir y no transmitir. En modo `no_verifactu` los registros encadenados se siguen generando y guardando en local — el modo cambia la transmisión, no el encadenamiento — y deben quedar disponibles para inspección, pero no se envía nada en tiempo real ([`BR-VFC-018`](#traceability), RD 1007/2023 art. 16). </Step> <Step> **Excepción de la importación histórica.** Las facturas cargadas por la importación masiva de histórico previo a la adhesión llevan una marca transitoria que hace que los manejadores de VeriFactu retornen sin crear registro alguno ([`BR-INV-011`](#traceability), [`BR-VFC-009`](#traceability)). Sin ella, importar años de histórico declararía miles de altas con fechas de expedición anteriores a la incorporación de la empresa al sistema. La marca la fuerza el importador y **no** se expone en los endpoints ordinarios de creación — no puedes activarla desde la API pública. </Step> <Step> **Certificado activo.** Firmar requiere el certificado FNMT propio de la empresa. Si no hay ninguno, o está caducado, revocado, o su NIF no coincide con el de la empresa, la creación del alta falla con un error de regla de negocio. Comprueba `has_active_certificate` en el endpoint de configuración antes de salir a producción. </Step> </Steps> Si pasan las cinco, el registro se crea, se encadena y se encola para transmitir. Que la cola transmita automáticamente es a su vez un ajuste de instancia, expuesto en solo lectura como `auto_transmit` en el endpoint de configuración. ## Qué envía la API [#api] Nada que escribas tú. No hay cuerpo de petición para «enviar», ni ningún campo en [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) que lo controle. Lo que sí controlas es *cuándo se emite la factura*, y de ahí se deriva todo lo demás: * Crea la factura como borrador y luego emítela con [`POST /v1/invoices/{id}/send`](/api-reference/invoices/public-api.v1.invoices.send) o [`POST /v1/invoices/{id}/mark-sent`](/api-reference/invoices/public-api.v1.invoices.mark_sent). * O créala y emítela de forma atómica pasando `options.issue_directly` en la llamada de creación. Como el camino de «crear y emitir en una sola llamada» emite a la vez un evento de creación y uno de emisión, dos manejadores compiten por crear la misma alta. El comando es **idempotente por factura**: el segundo detecta el alta existente y no hace nada en silencio, de modo que existe exactamente un registro por factura ([`BR-VFC-008`](#traceability)). No necesitas deduplicar por tu lado. ### La única vía de escape explícita [#force] *Sí* existe una operación que fuerza la creación de un alta para una factura ya emitida: [`POST /v1/invoices/{id}/verifactu`](/api-reference/verifactu/public-api.v1.invoices.verifactu_create), scope `verifactu:write`. Crea el alta y encola su transmisión, respondiendo `201` con el registro nuevo. Existe para el caso en que una factura se emitió con una compuerta cerrada —un certificado que todavía no se había subido, por ejemplo— y quieres el alta en cuanto la compuerta se abre. **No** es un reenvío: | Situación | Respuesta | | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | La factura ya tiene un alta | `422` `verifactu_already_submitted` | | La factura sigue en borrador, VeriFactu está desactivado en la instancia, o el certificado falta, está caducado, revocado o con NIF que no casa | `422` `verifactu_not_eligible` | | La factura no existe, o pertenece a otra empresa | `404` `invoice_not_found` | ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ### Las únicas palancas manuales sobre un registro que ya existe [#levers] Creada el alta, exactamente dos operaciones actúan sobre ella, y las dos se explican en [Estados de envío VeriFactu](/guides/verifactu-submission-states): * [`retry`](/api-reference/verifactu/public-api.v1.verifactu.records.retry) — reenvía sin cambios la declaración almacenada, para fallos técnicos. * [`subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) — regenera la declaración a partir de datos maestros corregidos, para rechazos de la AEAT. No existe operación que retransmita un registro aceptado. La aceptación es terminal por norma. ## Activar es un compromiso, no un interruptor [#commitment] Encender VeriFactu es asimétrico, y una integración que lo trate como un interruptor reversible se topará con un `422` en producción. Pasar al modo verificable siempre está permitido. **Volver atrás** está bloqueado hasta el 31 de diciembre del año en que se activó ([`BR-VFC-001`](#traceability), [`BR-VFC-023`](#traceability), RD 1007/2023 art. 13). La integridad de una cadena declarada a la AEAT en tiempo real no puede degradarse a software autocertificado a mitad de un ejercicio fiscal. Hay una escapatoria deliberada: mientras la cadena siga **vacía** —la empresa no ha emitido un solo registro de facturación en ningún estado— la empresa puede cambiar de idea y volver atrás, y el bloqueo se levanta. El primer registro emitido, aunque sea uno rechazado o con error, arma el bloqueo hasta fin de año. Apagar la bandera de activación por empresa lo impide la misma guarda, así que no sirve para esquivar el compromiso. `GET /v1/verifactu/config` expone `is_locked_until` para que puedas enseñárselo a tus usuarios antes de que se comprometan. ## Sandbox y producción no son intercambiables [#environments] Cada empresa opera contra un único entorno AEAT, expuesto como `environment` tanto en el objeto de configuración como en cada registro. Un CSV obtenido contra el entorno de pruebas de la AEAT **no** es un alta: los CSV de pruebas llevan un prefijo reconocible, y una base de datos de producción que los contenga significa que se simuló algo que debería haberse transmitido ([`BR-VFC-017`](#traceability)). La regla que te protege es que el sistema nunca debe caer en simulación de forma silenciosa — un endpoint inaccesible tiene que aflorar como estado técnico `error`, no como una aceptación fabricada. Cuando concilies, trata el campo `environment` como parte de la identidad del registro. ## Qué sale en el PDF [#pdf] El alta automática en sí no añade nada al documento; lo impreso depende de la *existencia* de un registro, no de cómo se creó. En cuanto existe un registro, la factura lleva el bloque QR legal ([`BR-VFC-015`](#traceability)), y la leyenda bajo el código difiere según el modo de funcionamiento: la marca corta `VERI*FACTU` en modo verificable, y la frase completa que declara que la factura es verificable en la sede electrónica de la AEAT en el otro. Una factura importada con la excepción de histórico no tiene registro y por tanto **no imprime QR**. Es lo correcto: las facturas anteriores a la adhesión no son verificables en la AEAT. ## Qué llega a la AEAT [#aeat] Una declaración de alta por factura emitida, encadenada al registro anterior de la empresa, más una declaración de anulación si la factura se anula después (ver [Anular o rectificar](/guides/annul-vs-correct)). No se transmite nada más como consecuencia de emitir. En modo `no_verifactu` no llega nada a la AEAT en tiempo real; la empresa conserva la cadena local para inspección y el sistema registra periódicamente resúmenes de sus propios eventos operativos, que la norma trata como evidencia separada ([`BR-VFC-018`](#traceability)). ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-VFC-001` — la adhesión al modo verificable es irrevocable hasta fin de año natural, con la excepción de la cadena vacía. * `BR-VFC-008` — idempotencia: un alta por factura, incluso cuando el flujo de crear y emitir dispara dos eventos. * `BR-VFC-009` — la excepción de la importación histórica, vista desde VeriFactu. * `BR-VFC-015` — el bloque QR y sus dos leyendas. * `BR-VFC-017` — la frontera entre sandbox y producción, y la prohibición de simular en silencio. * `BR-VFC-018` — modo `no_verifactu`: cadena local, resúmenes de eventos, sin transmisión en tiempo real. * `BR-VFC-023` — el cambio de modo asimétrico y el bloqueo hasta fin de año, incluida la guarda que impide esquivarlo con la bandera de activación. * `BR-VFC-025` — activación por empresa, desactivada de fábrica, valor efectivo como `instancia Y empresa`. * `BR-INV-011` — la excepción de la importación histórica, vista desde la facturación. --- # Estados de envío VeriFactu (/es/guides/verifactu-submission-states) Cada factura que emite tu empresa bajo VeriFactu produce un **registro de facturación**: una declaración XML firmada que se transmite a la AEAT y queda encadenada criptográficamente al registro anterior de la misma empresa. La factura y su registro son dos objetos distintos con dos ciclos de vida distintos — una factura puede estar `sent` y cobrada mientras su registro sigue `rejected` por la AEAT. Esta página trata del **registro**. Si integras contra Factuarea y solo vigilas el estado de la factura, no te enterarás de que la Administración tributaria rechazó una declaración. ## Cuándo aplica [#when] El ciclo de vida del registro aplica a toda empresa con VeriFactu efectivamente activado, desde el momento en que una factura sale de `draft`. **No** aplica a: * Empresas todavía en modo `no_verifactu`: los registros se siguen creando y encadenando en local, pero nunca se transmiten, así que quedan fuera del ciclo aceptado/rechazado ([`BR-VFC-018`](#traceability), RD 1007/2023 art. 16). * Facturas históricas importadas saltándose el paso de VeriFactu — no se crea registro alguno, de modo que no hay nada que consultar ([`BR-VFC-009`](#traceability)). Consulta [Alta automática en VeriFactu](/guides/verifactu-auto-submission) para las compuertas que deciden si el registro llega a crearse. ## Los cinco estados [#states] | `status` | Significado | ¿Terminal? | Qué haces | | ----------- | -------------------------------------------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------- | | `pending` | El registro existe y está encadenado, pero aún no se ha transmitido. | No | Nada. La transmisión está en cola. | | `submitted` | Enviado a la AEAT, a la espera de la respuesta definitiva. | No | Nada. Consultar. | | `accepted` | La AEAT registró la declaración. `aeat_csv` viene informado. | **Sí — inmutable** | Nada. Para corregir la factura, emite una [rectificativa](/guides/corrective-invoices). | | `rejected` | La AEAT lo rechazó por un error de **datos** (NIF del destinatario desconocido, esquema, totales). | Respuesta definitiva, pero reparable | Corrige los datos y luego `subsanar`. | | `error` | Fallo **técnico** de transmisión: timeout, AEAT inaccesible, problema de firma. | No | Nada, o forzar un `retry`. | La distinción que importa es `rejected` frente a `error`. `rejected` es la AEAT diciendo «he leído tu declaración y está mal». `error` es la declaración que nunca llegó. Se reparan con operaciones distintas, y confundirlas es el error de integración más habitual en este endpoint. `accepted` es el único estado genuinamente inmutable: la matriz de transiciones rechaza cualquier salida de él, porque el RD 1007/2023 hace inalterable un registro ya registrado. Todos los demás estados admiten una nueva transición, y eso es lo que hace posibles el reintento y la subsanación. ## Qué envía la API [#api] Nunca creas un registro con un payload — se crea por ti. Lo que haces es leerlo. La API v1 devuelve el objeto registro en [`GET /v1/verifactu/records/{id}`](/api-reference/verifactu/public-api.v1.verifactu.records.show), [`GET /v1/verifactu/records`](/api-reference/verifactu/public-api.v1.verifactu.records.list) y, indexado por factura, en [`GET /v1/invoices/{id}/verifactu`](/api-reference/verifactu/public-api.v1.invoices.verifactu_get): | Campo | Significado | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | Uno de los cinco estados de arriba. | | `type` | `ALTA` (la factura se emitió) o `ANULACION` (se anuló). | | `invoice_type` | El tipo de factura AEAT congelado en el momento de emitir: `F1`, `F2`, `F3`, `R1`–`R5`. | | `huella` | La huella SHA-256 de **este** registro, en hexadecimal mayúsculas. Es el eslabón al que apuntará el registro siguiente. | | `aeat_csv` | El *Código Seguro de Verificación* que devuelve la AEAT al aceptar. Vale `null` hasta entonces. Es el valor con el que concilias contra la Administración tributaria. | | `aeat_submission_id` | Nuestro identificador de transmisión, para conversaciones con soporte. | | `transmitted_at` | ISO 8601 de la última transmisión que llegó a la AEAT. Viene informado en `submitted` y `accepted`; vale `null` en `pending`, `rejected` y `error`. | | `environment` | El entorno AEAT **de esta empresa** — producción o el entorno de pruebas de la AEAT. Un CSV obtenido en pruebas no es un alta real. | | `is_simplificada` / `is_substitute_for_simplified` | Si la factura de origen era una `F2`, y si este registro sustituye facturas simplificadas mediante una `F3`. | Dos campos merecen su propio aviso. **La `huella` es identidad, no una suma de control que puedas recalcular.** Se calcula a partir del NIF del emisor, la serie y el número, la fecha de expedición, el tipo de factura, la cuota total, el importe total, la huella del registro *anterior* y la marca de tiempo de generación — en ese orden y formato exactos. Si cualquiera de esos valores cambia, la cadena se rompe y falla la prueba de integridad de toda la empresa ([`BR-VFC-013`](#traceability)). Por eso hay correcciones que no se pueden reparar en el sitio; ver [Reintentar o subsanar](#retry-vs-subsanar). **El `aeat_csv` se escribe una sola vez.** Al aceptarse se persiste y nunca se sobrescribe, ni siquiera si la AEAT devuelve el mismo CSV en una transmisión posterior. Un registro que pasa a `rejected` después de haber estado `submitted` conserva el CSV anterior a efectos de auditoría, así que un `aeat_csv` no nulo en un registro `rejected` es lo esperado, no un fallo ([`BR-VFC-016`](#traceability)). ```bash curl https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ```json { "data": { "id": "0197b3c9-1de2-7c40-8b71-a4d5e6f70123", "object": "verifactu_record", "type": "ALTA", "invoice_type": "F1", "invoice_number": "F-2026-0042", "date": "2026-05-15", "amount": 121.0, "status": "accepted", "huella": "9F2C1A0B7E4D6835A1C0B9E8D7F6A5B43C2D1E0F9A8B7C6D5E4F3A2B1C0D9E8F", "aeat_submission_id": "sub_0197b3c9", "aeat_csv": "FCT-2026-A1B2C3D4-E5F6", "environment": "production", "transmitted_at": "2026-05-15T09:41:02Z", "is_simplificada": false, "is_substitute_for_simplified": false, "created_at": "2026-05-15T09:40:58Z" } } ``` ### El presupuesto de reintentos existe, y no viaja en el payload [#retry-budget] Detrás de `error` hay un contador y una planificación. Una transmisión fallida se vuelve a encolar con retroceso exponencial, y el número de reintentos técnicos a ciegas está topado por ronda de transmisión; agotado el tope, un reintento manual adicional responde con un error de regla de negocio en lugar de volver a encolar ([`BR-VFC-006`](#traceability)). Junto al contador, el registro lleva una marca de incidencia técnica, que se levanta cuando fue la propia AEAT la que estuvo inaccesible y hubo que declarar la incidencia — se conserva incluso tras una aceptación posterior, a efectos de auditoría. **Ninguno de esos tres valores — el contador de intentos, el siguiente reintento programado y la marca de incidencia — se expone en el objeto registro de la v1.** Gobiernan el comportamiento que observas, pero hoy no puedes leerlos por la API pública. Lo que *sí* puedes observar es el estado en sí, `transmitted_at` y la cronología de auditoría del registro vía [`GET /v1/verifactu/records/{id}/activities`](/api-reference/verifactu/public-api.v1.verifactu.records.activities). No construyas en tu cliente un modelo adivinado de la planificación de reintentos: consulta el estado. ## Reintentar o subsanar [#retry-vs-subsanar] Ambas operaciones actúan sobre un registro que ya existe. No son intercambiables. | Estado del registro | Causa | Operación | Por qué | | --------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `error` | La declaración nunca llegó a la AEAT. | [`POST /v1/verifactu/records/{id}/retry`](/api-reference/verifactu/public-api.v1.verifactu.records.retry) | El XML almacenado es correcto. Se reenvía sin cambios. | | `rejected` | La AEAT lo leyó y rechazó los datos. | [`POST /v1/verifactu/records/{id}/subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) | Hay que regenerar el XML a partir de los datos maestros corregidos. | | `accepted` | — | Ninguna. | El registro es inmutable. Emite una [factura rectificativa](/guides/corrective-invoices). | | `rejected`, pero la corrección toca un campo de la huella | Se declaró mal el total, la fecha, el número, el NIF o el tipo de factura. | Ninguna — anular y volver a emitir. | Cambiar un campo de la huella invalidaría la cadena. `subsanar` lo rechaza de entrada. | Reintentar a ciegas consume el presupuesto de reintentos técnicos. La subsanación no: es una corrección manual deliberada con datos nuevos, la norma no le pone tope y ejecutarla **reinicia la ronda de transmisión** — el contador de intentos vuelve a cero y la retransmisión automática del contenido corregido recupera su presupuesto íntegro ([`BR-VFC-006`](#traceability), [`BR-VFC-020`](#traceability)). La subsanación regenera el payload pero **incrusta la huella original, el eslabón de cadena original y la marca de tiempo de generación original**, porque son los que permiten a la AEAT casar el reenvío con el registro que rechazó. Antes de persistir nada compara los campos regenerados que entran en la huella con los almacenados; si alguno difiere, responde `422` con el subcódigo que te indica que hace falta anular, y no se modifica nada. El flujo completo, los subcódigos de error y los consejos de prevención están en [Subsanación de registros VeriFactu](/guides/verifactu-subsanacion). ## Qué sale en el PDF [#pdf] El estado del registro **no** cambia el PDF. Sea cual sea el estado, la factura imprime el mismo bloque QR legal en la esquina superior derecha de la primera página: la etiqueta `QR tributario:`, un código de 30×30 mm que apunta al servicio de verificación de la AEAT con el NIF del emisor, la serie y el número, la fecha y el total, y la leyenda debajo ([`BR-VFC-015`](#traceability)). Tres consecuencias que conviene contemplar en el diseño: * El QR se imprime en cuanto existe un registro — incluso mientras está `pending`, `error` o `rejected`. Un destinatario que lo escanee antes de la aceptación verá que la AEAT no informa de ningún alta. Es el comportamiento correcto, no un defecto. * El CSV **no** se imprime en el PDF. Solo está disponible por la API y en el panel. * La huella y la marca de tiempo del alta también dejaron de imprimirse. Si los estabas extrayendo del PDF, léelos del registro. La subsanación es la única operación que además toca el documento impreso: vuelve a congelar deliberadamente los snapshots inmutables de destinatario y emisor a partir de los datos maestros actuales, para que el PDF coincida con lo que se volvió a declarar a la AEAT ([`BR-INV-024`](#traceability), [`BR-VFC-020`](#traceability)). Es el único camino que reescribe un snapshot ya congelado. ## Qué llega a la AEAT [#aeat] Cada registro transmite una declaración, encadenada por su huella al registro anterior de la misma empresa. Existen tres clases de registro, y no comparten una sola cadena: las altas (`ALTA`) y las anulaciones (`ANULACION`) comparten la cadena de facturación, mientras que los registros de eventos del sistema mantienen una cadena propia aparte, porque la norma trata los eventos operativos como evidencia separada ([`BR-VFC-014`](#traceability)). Cuando un reenvío sigue a un rechazo, la declaración regenerada lleva además las marcas AEAT que declaran que el envío anterior fue rechazado y que, por tanto, el registro nunca llegó a registrarse. Ninguna de las dos entra en el cálculo de la huella, así que declararlas no perturba la cadena ([`BR-VFC-026`](#traceability)). Puedes verificar la cadena entera por tu cuenta con [`GET /v1/verifactu/chain/validate`](/api-reference/verifactu/public-api.v1.verifactu.chain.validate), que recalcula todas las huellas e informa de las anomalías. Está limitado a una llamada por minuto y empresa porque recorre el libro registro completo. ## Trazabilidad [#traceability] Derivado de las reglas de dominio del backend de Factuarea: * `BR-VFC-006` — política de reintentos: retroceso exponencial, intentos topados por ronda, y el tope que explícitamente no aplica a la subsanación. * `BR-VFC-013` — la cadena de huellas es inmutable y verificable; cualquier alteración invalida la garantía de integridad ante la AEAT. * `BR-VFC-014` — las tres clases de registro y sus cadenas independientes. * `BR-VFC-015` — el bloque QR obligatorio en la factura impresa. * `BR-VFC-016` — el CSV como identidad pública del registro, persistido sin alterar. * `BR-VFC-018` — modo `no_verifactu`: cadena local, sin transmisión. * `BR-VFC-020` — subsanación de registros rechazados, guarda de la huella y reinicio de la ronda de transmisión. * `BR-VFC-026` — las marcas AEAT para un reenvío tras rechazo. * `BR-INV-024` — el snapshot inmutable del destinatario y la única excepción que lo refresca. Derivado también de la máquina de estados de transmisión documentada junto a esas reglas (`AeatTransmissionStatus`), que es la fuente de verdad de la matriz de transiciones citada en [Los cinco estados](#states). --- # Subsanación de registros VeriFactu (/es/guides/verifactu-subsanacion) Cuando la AEAT rechaza un registro de facturación VeriFactu por un **error de datos** (registro con `status: rejected`), la normativa VeriFactu (Real Decreto 1007/2023, art. 11) te permite **subsanar** el registro: reenviar el **mismo registro** con el contenido corregido. No es una factura nueva, ni una rectificativa, ni una anulación — el propio registro rechazado se repara y se transmite de nuevo. ``` POST /v1/verifactu/records/{record}/subsanar ``` * **Scope:** `verifactu:write` * **Body de la request:** ninguno — el contenido subsanable se regenera en el servidor desde la factura origen y los datos maestros **actuales**. * **Respuesta:** `202 Accepted` — el reenvío se encola y se envía a la AEAT en unos segundos. <Callout type="info"> La subsanación **nunca recalcula** la huella original, el eslabón de encadenamiento con el registro anterior ni el sello de generación original: la AEAT casa el reenvío con el registro rechazado precisamente porque se conservan. **No hay tope de intentos de subsanación** — el límite de reintentos técnico aplica solo a los reintentos ciegos de fallos de transmisión. </Callout> ## Cuándo usarla — y cuándo no [#when] | Situación | Qué hacer | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Registro **`rejected`** por la AEAT por un error de datos — NIF del destinatario no identificado en el censo, razón social incorrecta, problemas en la descripción… | **Subsanación.** Corrige los datos en su origen y llama a `POST …/subsanar`. | | Registro en **`error`** (fallo técnico de transmisión: timeout, AEAT caída). | Nada — la transmisión se reintenta automáticamente ([reintento manual](/api-reference/verifactu/public-api.v1.verifactu.records.retry) disponible). La subsanación responde `record_not_rejected`. | | Registro **`accepted`** pero la factura lleva datos incorrectos. | Una **factura rectificativa** (R1–R5). Un registro aceptado es inmutable — la subsanación responde `record_not_rejected`. | | La corrección cambia un **campo de la huella**: NIF del emisor, serie + número, fecha de expedición, tipo de factura, cuota total o importe total. | **Anulación + registro nuevo** (factura nueva o rectificativa). La subsanación responde `requires_annulment` sin modificar nada. | ## El flujo [#flow] <Steps> <Step> **Detecta el rechazo.** Suscríbete al [evento de webhook](/guides/webhooks) `invoice.verifactu_failed`, o consulta el [listado de registros](/api-reference/verifactu/public-api.v1.verifactu.records.list) buscando `status: rejected`. El registro lleva el detalle del rechazo de la AEAT. </Step> <Step> **Corrige los datos en su origen.** El contenido reenviado se regenera desde la factura origen y los datos maestros actuales — p. ej. corrige el NIF o la razón social del cliente y los valores nuevos se recogen automáticamente. El snapshot legal congelado de la factura se refresca deliberadamente para que el PDF coincida con lo que recibe la AEAT. </Step> <Step> **Llama al endpoint.** El registro vuelve a entrar en la cola de transmisión con una ronda de intentos nueva: ```bash curl -X POST https://api.factuarea.com/v1/verifactu/records/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/subsanar \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Respuesta (`202`): ```json { "data": { "id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "message": "Subsanación encolada. El registro se reenviará a la AEAT en unos segundos." } } ``` </Step> <Step> **Vigila el resultado.** El registro se transmite de nuevo y termina en `accepted` — o en `rejected` otra vez si los datos siguen mal, en cuyo caso puedes volver a subsanar (no hay límite de intentos). </Step> </Steps> ## Errores [#errors] Las violaciones de regla de negocio devuelven `422` con `code: business_rule_violation` y un `subcode` que concreta la causa: ```json { "error": { "type": "invalid_request_error", "code": "business_rule_violation", "subcode": "record_not_rejected", "message": "El registro #842 no está rechazado por la AEAT (estado actual: accepted). Solo los registros rechazados admiten subsanación; para fallos técnicos usa el reintento." } } ``` | HTTP | `code` / `subcode` | Cuándo | | ---- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | 404 | `resource_not_found` | El registro no existe o pertenece a otra empresa. | | 422 | `business_rule_violation` / `record_not_rejected` | El registro no está en `rejected` (está aceptado, pendiente, enviado — o en `error` técnico, que ya cubre el reintento automático). | | 422 | `business_rule_violation` / `requires_annulment` | La corrección toca campos de la huella. Anula el registro y emite una factura nueva o una rectificativa. | | 422 | `business_rule_violation` / `record_not_subsanable` | El registro no es de *alta*, o no tiene factura origen desde la que regenerar. | | 403 | `insufficient_scope` | La key no tiene el scope `verifactu:write`. | ## Evita el rechazo antes de que ocurra [#prevention] El rechazo de datos más frecuente es un destinatario que el censo de la AEAT no identifica (rechazo **1239**). Verifica el **par nombre + NIF** de un cliente con [`POST /v1/clients/census-verification`](/guides/census-verification#clients) **antes** de emitirle facturas VeriFactu — así la subsanación se queda en lo que debe ser: una red de seguridad, no una rutina. --- # Versionado (/es/guides/versioning) La API de Factuarea sigue una política de URL con **versionado plano** (`/v1`) combinada con una cabecera de fecha opcional para una evolución sin cambios incompatibles. El compromiso es claro: una vez publicada, `/v1` se mantiene estable. Los cambios incompatibles requieren `/v2`. ## Versión en la URL [#versión-en-la-url] ``` https://api.factuarea.com/v1/... ``` `v1` es nuestra primera versión pública (mayo de 2026). No hay versiones anteriores accesibles. Cuando se diseñe `/v2`: * `/v1` y `/v2` coexisten durante **al menos 12 meses**. * Las rutas de `/v1` no cambian en esa ventana (ni payloads, ni status codes, ni campos, ni semántica). * Avisos por email a los desarrolladores con keys activas, banner en la documentación, cabeceras en las respuestas (ver más abajo). ## Cabecera `Factuarea-Version` [#cabecera-factuarea-version] ```http Factuarea-Version: 2026-06-01 ``` El date-versioning funciona al estilo de Stripe. Hay un **registro de versiones soportadas** (fechas `YYYY-MM-DD`); hoy hay una sola, `2026-06-01`, que es también la última. La versión que aplica a una petición — la **versión efectiva** — se resuelve en este orden: 1. La cabecera de petición `Factuarea-Version`, si la envías. 2. Si no, la versión **fijada en tu API key** (se establece al crear la key; null significa "siempre la última"). 3. Si no, la **última** versión del registro. La versión efectiva se **devuelve en cada respuesta** en la cabecera `Factuarea-Version`, así siempre sabes qué versión por fecha sirvió tu petición. ```http Factuarea-Version: 2026-06-01 ``` Fijar una versión (por cabecera o en la key) congela el comportamiento del subconjunto de endpoints que reciben mejoras incrementales sin cambios incompatibles (nuevos campos en la respuesta, nuevos parámetros opcionales). Sin cabecera ni fijación, obtienes la última versión. El registro tiene hoy dos fechas: **`2026-06-01`** (la predeterminada, y la que obtienes sin cabecera ni fijación) y **`2026-09-01`**. Optar por la más reciente cambia dos cosas, y ninguna más: | Cambio en `2026-09-01` | Qué obtienes en `2026-06-01` | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Las respuestas de borrado masivo usan la forma transversal de éxito parcial `{total, successful, failed, failures[{id, error_code, error_message}]}`. | La forma anterior `{object: "bulk_delete_result", deleted, failed[{id, reason}]}`. | | Los cinco errores preexistentes de los gates de cobro (`payment_method_required`, `seat_charge_failed`, `gestoria_plan_required`, `employee_seat_payment_method_required`, `employee_seat_charge_failed`) llevan `error.type: "payment_required_error"`. | `error.type: "invalid_request_error"` en esos cinco, exactamente como antes. | <Callout type="info"> La reclasificación del error **no** toca `error.code`, `error.subcode` ni el estado HTTP — son `402` con el mismo código en todas las versiones. Si ramificas por `code` (que es lo que recomendamos), no cambia nada para ti en ningún caso. `addon_required` es un código posterior y lleva siempre `payment_required_error`. </Callout> ### Errores [#errores] La cabecera se valida contra el registro: * Valor **mal formado** (que no sea `YYYY-MM-DD`, p. ej. `2026-05` o `15/05/2026`) → `400 parameter_invalid_format` con `param: "Factuarea-Version"`. * **Bien formado pero no soportado** (una fecha válida que no está en el registro) → `400 unsupported_api_version` con `param: "Factuarea-Version"`. Omitir la cabecera nunca es un error — cae a la fijación de la key o a la última versión. ## ¿Qué es un cambio incompatible? [#qué-es-un-cambio-incompatible] Consideramos **incompatible** (prohibido en `/v1`): * Renombrar / eliminar campos de la respuesta JSON. * Cambiar el tipo de un campo (`string` → `int`). * Cambiar el `type`/`code` de un envoltorio de error existente. * Cambiar status codes (p. ej. devolver `201` donde antes era `200`). * Convertir en obligatorio un campo de la petición que antes era opcional. * Cambiar el formato de un identificador (UUID v7 sigue siendo UUID v7). * Eliminar un endpoint sin un reemplazo documentado y una ventana de migración. * Cambiar la semántica de la máquina de estados de los documentos. Consideramos **no incompatible** (permitido sin una nueva versión): * Añadir campos nuevos en las respuestas. * Añadir parámetros opcionales en las peticiones. * Añadir endpoints nuevos. * Relajar restricciones (subir un límite, aceptar más formatos). * Añadir valores de enum nuevos **a campos que no sean críticos para las máquinas de estados del lado del cliente**. * Mejorar los mensajes de error (cambia `message`, no `type`/`code`). * Reclasificar el `type` de un envelope de error existente **detrás de una versión por fecha**: las keys fijadas a una fecha anterior siguen recibiendo el `type` previo tal cual, y `code`/`subcode`/estado no se mueven. Así se recategorizaron los cinco errores de cobro en `2026-09-01`. Hacerlo *sin* versión por fecha es el caso incompatible que aparece arriba. ## Política de deprecación [#política-de-deprecación] Cuando un endpoint o campo se marca como deprecado dentro de `/v1` (p. ej. un alias heredado reemplazado por una versión canónica): * Aviso por email a los desarrolladores con keys activas afectadas. * Banner en `docs.factuarea.com` con el changelog. * Cabeceras en cada respuesta del endpoint deprecado durante **al menos 12 meses** antes de su retirada (que solo ocurre en `/v2`): ```http Deprecation: true Sunset: Wed, 15 May 2027 00:00:00 GMT Link: <https://docs.factuarea.com/changelog#v1-deprecations>; rel="deprecation" Link: <https://docs.factuarea.com/guides/migration-from-holded>; rel="alternate" ``` * En `/v1` el endpoint **sigue funcionando** hasta el lanzamiento de `/v2`. Las cabeceras avisan. * En `/v2` el endpoint se retira / reemplaza. La ventana entre el primer aviso y `/v2` es ≥ 12 meses. ## Migración entre versiones [#migración-entre-versiones] Cada migración (`v1 → v2`) viene acompañada de: * Una guía dedicada en `docs.factuarea.com/guides/migration-v1-v2`. * Mapeo campo a campo y endpoint a endpoint. * Recomendaciones operativas (mantener ambas keys, escritura dual durante la transición). * Webhooks: los eventos antiguos conservan su forma; los eventos nuevos viven en su propia versión declarada en el payload. ## Changelog [#changelog] Cada cambio de `/v1` (campo nuevo, deprecación, evento nuevo, corrección de validación) se publica en [Changelog](/changelog/launch) con etiquetas: * `feature` — campo / endpoint / evento nuevo. * `fix` — corrección de un bug. * `deprecation` — campo o endpoint marcado como obsoleto (todavía activo en `/v1`). * `breaking` — solo aparece en `/v2`, nunca dentro de `/v1`. * `security` — corrección con implicaciones de seguridad. Léela primero. Suscríbete al feed RSS en `https://docs.factuarea.com/changelog.rss` o sigue `@factuarea` en X para los anuncios. ## Compromiso de estabilidad [#compromiso-de-estabilidad] <Callout type="info"> Una integración construida hoy contra `/v1` seguirá funcionando en `/v1` durante **al menos 24 meses** desde hoy, sin tocar tu código. Ventana de soporte para v1 → mínimo 12 meses tras el lanzamiento de v2. </Callout> Esa es la garantía. Cualquier excepción se comunicará con plazos generosos. --- # Webhooks (/es/guides/webhooks) Los webhooks notifican a tu servidor cuando ocurre un evento en Factuarea (factura pagada, presupuesto aceptado, cliente creado, etc.) sin que tengas que hacer polling. Cada evento se entrega a tu URL mediante un `POST` HTTPS firmado. <Callout type="warn"> **No se entregan en modo de prueba.** Los eventos generados con una clave `fact_test_` (sandbox) se registran pero **nunca se entregan** a tus endpoints externos. Para ejercitar la verificación de firma de tu receptor en sandbox, usa el endpoint dedicado `POST /v1/webhook_endpoints/{id}/ping`, que *sí* se entrega. Consulta [Modo de prueba y sandbox](/guides/test-mode). Para validar tu handler real end-to-end, usa [`test_event`](#test-deliveries) en su lugar. </Callout> ## Crear un endpoint [#crear-un-endpoint] ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.mycompany.com/factuarea/webhook", "description": "Sync with internal CRM", "enabled_events": [ "invoice.created", "invoice.paid", "quote.approved" ] }' ``` Respuesta (el `secret` se devuelve **solo una vez**): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "webhook_endpoint", "url": "https://app.mycompany.com/factuarea/webhook", "description": "Sync with internal CRM", "enabled_events": ["invoice.created", "invoice.paid", "quote.approved"], "status": "enabled", "secret": "whsec_01HKQS5N8VR7QXJ9K3T6BWPMZA9876543210ABCDEF", "created_at": "2026-05-15T10:23:18Z" } ``` Para suscribirte a **todos los eventos**, pasa `"enabled_events": ["*"]`. El catálogo completo está en [Eventos](/guides/events). ## Firma HMAC SHA256 [#firma-hmac-sha256] Cada entrega incluye estas headers: ```http Factuarea-Signature: t=1747314060,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd Factuarea-Event-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d Factuarea-Event-Type: invoice.paid Factuarea-Delivery-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c Idempotency-Key: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d ``` * `t` — timestamp UNIX de la entrega (segundos). * `v1` — HMAC SHA256 de la cadena `{t}.{body}` con el `secret` del endpoint, en hex. * `Idempotency-Key` — el header estándar de la industria, con el **mismo valor** que `Factuarea-Event-Id` (el UUID v7 estable del evento). Úsalo para deduplicar reentregas con el header que tu stack quizá ya entiende. Consulta [Idempotencia por tu parte](#idempotency-on-your-side). <Callout type="info"> **¿Usas un SDK oficial?** Sáltate el HMAC manual de abajo — tanto el [SDK de TypeScript como el de PHP](/sdks) incluyen un verificador de webhooks que hace por ti la comparación en tiempo constante, la tolerancia del timestamp y la gestión del periodo de gracia de rotación. Consulta [Verificar webhooks con el SDK](/sdks#verifying-webhooks). La receta manual de abajo es para cualquier otro lenguaje. </Callout> Para validar: 1. Extrae `t` y `v1` de `Factuarea-Signature`. 2. Calcula `signed_payload = t + "." + raw_body` (bytes crudos del body, sin reformatear el JSON). 3. Calcula `expected = hmac_sha256(secret, signed_payload)` en hex. 4. Compara `v1 == expected` usando **comparación en tiempo constante**. 5. Comprueba que `|now - t| <= 300` (tolerancia de ±5 minutos contra ataques de replay). <Callout type="info"> Durante el periodo de gracia de una rotación de secret la header lleva **dos** valores `v1` — uno por cada secret activo (`t=...,v1=<current>,v1=<previous>`). Acepta la petición si coincide **cualquiera** de los `v1`. Consulta [Rotación de secret](#secret-rotation-dual-signing). </Callout> <Tabs items="['PHP', 'Node.js', 'Python (Flask)']"> <Tab value="PHP"> ```php function verifyFactuareaSignature( string $payload, string $signatureHeader, string $secret, int $toleranceSeconds = 300, ): bool { $timestamp = null; $signatures = []; foreach (explode(',', $signatureHeader) as $kv) { [$k, $v] = explode('=', $kv, 2); if ($k === 't') { $timestamp = (int) $v; } elseif ($k === 'v1') { $signatures[] = $v; } } if ($timestamp === null || $signatures === []) { return false; } if (abs(time() - $timestamp) > $toleranceSeconds) { return false; } $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret); foreach ($signatures as $candidate) { if (hash_equals($expected, $candidate)) { return true; } } return false; } // In the webhook handler: $payload = file_get_contents('php://input'); $header = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; $secret = getenv('FACTUAREA_WEBHOOK_SECRET'); if (! verifyFactuareaSignature($payload, $header, $secret)) { http_response_code(401); exit; } $event = json_decode($payload, true); handleEvent($event); http_response_code(200); ``` </Tab> <Tab value="Node.js"> ```javascript const crypto = require('crypto'); function verifySignature(payload, header, secret, tolerance = 300) { let timestamp = null; const signatures = []; for (const kv of header.split(',')) { const [k, v] = kv.split('='); if (k === 't') timestamp = Number(v); else if (k === 'v1') signatures.push(v); } if (timestamp === null || signatures.length === 0) return false; if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${payload}`) .digest('hex'); return signatures.some((candidate) => crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(candidate, 'hex') ) ); } // Express: app.post('/factuarea/webhook', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString('utf8'); if (!verifySignature(payload, req.header('Factuarea-Signature'), process.env.WHSEC)) { return res.status(401).end(); } const event = JSON.parse(payload); handleEvent(event); res.status(200).end(); }); ``` </Tab> <Tab value="Python (Flask)"> ```python import hmac, hashlib, time from flask import request, abort def verify(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool: timestamp = None signatures = [] for kv in header.split(','): k, v = kv.split('=', 1) if k == 't': timestamp = int(v) elif k == 'v1': signatures.append(v) if timestamp is None or not signatures: return False if abs(time.time() - timestamp) > tolerance: return False signed = f"{timestamp}.".encode() + payload expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, candidate) for candidate in signatures) @app.post('/factuarea/webhook') def webhook(): if not verify(request.get_data(), request.headers.get('Factuarea-Signature', ''), WHSEC): abort(401) event = request.get_json() handle_event(event) return '', 200 ``` </Tab> </Tabs> ## Verificar con el SDK oficial [#verificar-con-el-sdk-oficial] Los [SDK de TypeScript y PHP](/sdks) envuelven los cinco pasos de arriba — comparación en tiempo constante, tolerancia de ±5 minutos y periodo de gracia de rotación — en una sola llamada. Pasa el **body crudo de la petición**, la header `Factuarea-Signature` y el secret del endpoint: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Express, with express.raw({ type: "application/json" }) on the route: app.post("/webhooks/factuarea", (req, res) => { try { const event = factuarea.webhooks.verify( req.body.toString("utf8"), req.headers[SIGNATURE_HEADER.toLowerCase()] as string, process.env.FACTUAREA_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { /* … */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` Una tolerancia personalizada (en segundos) es el cuarto argumento opcional: `factuarea.webhooks.verify(body, header, secret, { toleranceSeconds: 600 })`. </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier; use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException; $verifier = new WebhookVerifier(); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; try { $event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET')); // $event is the decoded, authenticated payload if (($event['type'] ?? null) === 'invoice.paid') { /* … */ } http_response_code(200); } catch (WebhookSignatureException $e) { http_response_code(400); } ``` </Tab> </Tabs> Ambos verificadores aceptan **las dos** firmas `v1` durante el periodo de gracia de una rotación de secret (consulta [Rotación de secret](#secret-rotation-dual-signing)), de modo que una rotación nunca descarta una entrega. ## Reintentos [#reintentos] Si tu endpoint responde con un status que **no es `2xx`** o no responde dentro del `timeout_seconds` del endpoint (por defecto 10 s), Factuarea reintenta con back-off exponencial: | Intento | Espera tras el anterior | | ------- | ----------------------- | | 1 | inmediato | | 2 | 1 minuto | | 3 | 5 minutos | | 4 | 30 minutos | | 5 | 2 horas | | 6 | 12 horas | | 7 | 1 día | | 8 | 3 días | Tras el intento final la entrega pasa a `failed_permanently` y deja de reintentarse. Permanece visible en `GET /v1/webhook_endpoints/{id}/deliveries` durante 30 días, y puedes reintentarla manualmente mediante `POST /v1/webhook_endpoints/{id}/deliveries/{delivery_id}/replay` o desde el dashboard. ## Body de la entrega [#body-de-la-entrega] El body de la entrega es el propio [objeto de evento](/guides/events): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "api_version": "2026-05-22", "livemode": true, "test": false, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } } } ``` El campo `data` contiene una **referencia ligera** al recurso afectado — recupéralo desde su propio endpoint para obtener la representación completa. El `api_version` está siempre presente en los eventos entregados (`null` solo para eventos antiguos emitidos antes de sellar las versiones). El campo `test` está siempre presente: `true` solo para una **entrega de prueba** disparada desde el dashboard (ver abajo), `false` para eventos reales. Es **ortogonal a `livemode`**: `test` indica si *esta entrega* es de prueba, mientras que `livemode` refleja el **entorno de la key** (producción vs sandbox). Una entrega de prueba puede emitirse en cualquiera de los dos, así que `livemode: true, test: true` es válido. ## Respuesta esperada [#respuesta-esperada] * Status `200`, `201`, `202` o `204` → entrega `delivered`. * Cualquier otro status → entrega `failed`, se programa el siguiente reintento. * El body es irrelevante. **No** lo procesamos — solo se guardan `response_status` y `duration_ms` en el log de entregas. ## Replay desde el dashboard [#replay-desde-el-dashboard] `Developers > Webhooks > Deliveries` te permite reintentar manualmente cualquier entrega, incluso las `failed_permanently`. Un reintento manual reinicia el contador y deja una entrada en el log de auditoría. ## Entregas de prueba [#test-deliveries] Dos endpoints te permiten ejercitar tu receptor sin esperar a un evento real — resuelven problemas distintos: * `POST /v1/webhook_endpoints/{id}/ping` envía un payload **sintético** `webhook.ping`. Nunca entra en el log de eventos ni es un tipo de evento real. Úsalo para confirmar la accesibilidad y la verificación de firma (es la única entrega que se dispara en **sandbox**). * `POST /v1/webhook_endpoints/{id}/test_event` dispara una **entrega de prueba de un tipo de evento real del catálogo**, marcada con `"test": true` en el envelope. Registra un `Event` real (visible en `GET /v1/events`) y encola un `WebhookDelivery` firmado y reintentado **exactamente igual que una entrega de producción** — así validas tu handler real end-to-end. ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints/{id}/test_event \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "invoice.paid" }' ``` `type` es opcional: omítelo para usar el primer evento suscrito del endpoint. Si lo indicas, debe ser uno de los `enabled_events` del endpoint (si no, `422 event_not_subscribed`). La entrega llega **solo a este endpoint**, nunca a los demás endpoints suscritos al mismo tipo. ## Rotación de secret (dual-signing) [#secret-rotation-dual-signing] ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints/{id}/rotate_secret \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Devuelve el nuevo secret. Durante **24 horas** (el instante `previous_secret_valid_until` de la respuesta) ambos secrets son válidos: cada entrega se firma **dos veces** en la misma header `Factuarea-Signature` — un `v1` por secret (`t=...,v1=<current>,v1=<previous>`). Tras la ventana, el secret antiguo queda invalidado. Te permite desplegar el nuevo secret con cero tiempo de inactividad: 1. Llama a `/rotate_secret` → obtén el nuevo `secret`. 2. Despliega el nuevo secret en tu entorno. 3. Tu handler acepta cualquiera de los `v1` durante el periodo de gracia (los helpers de verificación de arriba ya recorren todos los `v1`). 4. Tras la ventana, solo el nuevo secret está en uso. ## Idempotencia por tu parte [#idempotency-on-your-side] Cada evento incluye un campo `id` (UUID v7, único). Los reintentos del mismo evento siempre llevan el mismo `id` — y el header `Idempotency-Key` lleva ese mismo valor exacto, así que puedes deduplicar desde el header sin parsear el body. Persiste los ids que hayas procesado (tabla `webhook_events_processed`) y devuelve `200` sin actuar si ya lo has procesado. ```python event_id = event['id'] if db.exists('webhook_events_processed', id=event_id): return '', 200 process(event) db.insert('webhook_events_processed', id=event_id, processed_at=now()) return '', 200 ``` ## Lista de acceso de IP (opcional) [#lista-de-acceso-de-ip-opcional] Si tu endpoint corre detrás de un firewall que filtra por IP, puedes restringir las IPs de origen mediante `ip_allowlist` al crear el endpoint. Factuarea entrega desde un pool de IPs estables documentadas en el dashboard. <Callout type="warn"> **Valida la firma HMAC, no la IP** — las IPs pueden cambiar con 30 días de aviso, las firmas no. </Callout> ## Eventos disponibles [#eventos-disponibles] El catálogo completo lo devuelve `GET /v1/event-catalog` y está documentado en [Eventos](/guides/events). Ejemplos clave: * `invoice.created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.annulled` * `quote.created`, `quote.approved`, `quote.rejected`, `quote.converted` * `proforma.accepted`, `proforma.converted_to_invoice` * `delivery_note.signed` * `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested` * `client.created`, `client.updated` --- # Horarios de trabajo (/es/guides/work-schedules) Un **horario de trabajo** modela las horas que una empresa **espera** de un empleado: cuántas horas al día y a qué hora empieza la jornada. Alimenta dos cálculos aguas abajo — las **horas esperadas** que usan los saldos, y la **hora planificada de entrada** que usa la [presencia](/guides/presence) para marcar llegadas tarde. Los horarios están acotados por `work_schedules:read` / `work_schedules:write` bajo `https://api.factuarea.com/v1`. ## El horario semanal [#schedule] Un **horario semanal** lleva un nombre, un **patrón semanal** de siete días —cada día una lista de franjas `HH:MM–HH:MM` no solapadas—, un **modo** y un estado (`active` / `archived`). Las horas semanales esperadas y la hora planificada se **derivan** del patrón. El **modo** fija cómo se mide el cumplimiento: | Modo | Significado | | --------------- | --------------------------------------------------------------------------------------------------- | | `validated` | Las horas esperadas se toman como trabajadas una vez validadas — el horario es la fuente de verdad. | | `real_clocking` | El cumplimiento se mide contra los fichajes reales del ledger. | El modo por defecto es `validated`. | Operación | Endpoint | | ---------------------- | ---------------------------------------------------------------- | | Listar / detalle | `GET /v1/work-schedules`, `GET /v1/work-schedules/{schedule}` | | Crear / actualizar | `POST /v1/work-schedules`, `PATCH /v1/work-schedules/{schedule}` | | Archivar / desarchivar | `POST /v1/work-schedules/{schedule}/archive`, `.../unarchive` | | Estadísticas | `GET /v1/work-schedules/stats` | ```bash curl -X POST https://api.factuarea.com/v1/work-schedules \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Jornada completa 9 a 17", "mode": "validated", "week_pattern": { "monday": [{ "start": "09:00", "end": "17:00" }], "tuesday": [{ "start": "09:00", "end": "17:00" }], "wednesday": [{ "start": "09:00", "end": "17:00" }], "thursday": [{ "start": "09:00", "end": "17:00" }], "friday": [{ "start": "09:00", "end": "17:00" }], "saturday": [], "sunday": [] } }' ``` Un día con lista vacía es un día de descanso. Consulta los esquemas en la [Referencia de API](/api-reference/work-schedules/public-api.v1.work_schedules.create). ## Asignaciones [#assignments] Un horario se aplica a un empleado mediante una **asignación efectivo-datada**: un `effective_from` (inclusivo) y un `effective_to` (exclusivo) opcional. Asignar un horario nuevo a un empleado **cierra la asignación abierta anterior**, así que un empleado tiene un horario efectivo en cualquier fecha sin huecos ni solapamientos. | Operación | Endpoint | Efecto | | -------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------- | | Asignar | `POST /v1/work-schedules/{schedule}/assign` | Abre una asignación desde `effective_from`, cerrando la anterior. | | Desasignar | `POST /v1/work-schedules/{schedule}/unassign` | Cierra la asignación abierta del empleado a este horario. | | Listar asignaciones | `GET /v1/work-schedules/{schedule}/assignments` | Los empleados asignados actualmente. | | Resolver el horario del empleado | `GET /v1/work-schedules/employee/{employee}` | El horario efectivo de un empleado en una fecha dada. | ```bash curl -X POST https://api.factuarea.com/v1/work-schedules/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/assign \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "effective_from": "2026-01-07" }' ``` `GET /v1/work-schedules/employee/{employee}` es el contrato que consumen los saldos y la presencia: devuelve el horario vigente del empleado en la fecha pedida, del que se leen las horas esperadas y la hora planificada. <Callout type="info"> Las asignaciones son datadas por rango, no un campo suelto en el empleado. Reasignar un horario nunca reescribe el historial — la asignación anterior se cierra con un `effective_to`, y la nueva se abre desde su `effective_from`. </Callout> ## Flujo típico [#flow] 1. Crea un **horario semanal** con su patrón semanal y su modo. 2. **Asígnalo** a los empleados desde una fecha `effective_from`. 3. Aguas abajo, el horario alimenta las **horas esperadas** de los saldos y la **hora planificada** que usa la [presencia](/guides/presence) para marcar llegadas tarde. 4. **Desasigna** o reasigna a medida que cambian los contratos; **archiva** los horarios que ya no uses. ## Próximos pasos [#next] * [Presencia](/guides/presence) — cómo la hora planificada activa la detección de llegadas tarde. * [Cierre mensual](/guides/monthly-time-close) — dónde se informan las horas esperadas frente a las trabajadas. --- # Visión general del control horario (/es/guides/workforce-overview) El **control horario** de Factuarea cubre la obligación legal de las empresas españolas según el **RD-ley 8/2019** (art. 34.9 del Estatuto de los Trabajadores): llevar un registro **objetivo, fiable e inalterable** de la jornada diaria de cada empleado, conservarlo **cuatro años** y ponerlo a disposición de la Inspección de Trabajo (ITSS). El registro se apoya en un **ledger de solo apéndice** sellado por una **cadena de huellas SHA-256 por empresa** — el mismo patrón antimanipulación que Factuarea aplica a la facturación [VeriFactu](/guides/glossary). Es, en resumen, el VeriFactu del fichaje: nada se edita ni se borra nunca, y cualquier manipulación rompe la cadena. Cada operación vive bajo `https://api.factuarea.com/v1` y comparte el mismo [envoltorio de error](/guides/errors), [paginación por cursor](/guides/pagination) y [scopes](/guides/scopes-and-irreversibility) que el resto de la API. Toda la superficie está gateada por el **módulo `control_horario`**; una empresa que no lo tenga recibe un `403` en estas rutas. ## El empleado, un rol solo-portal [#employee-role] Un **empleado** es la persona trabajadora que ficha, tiene horario, solicita ausencias y devenga saldos de jornada. Es un rol **solo-portal**: los empleados gestionan sus propios datos desde el portal y **nunca** computan contra el límite de asientos `users` del plan. Dar de alta empleados se factura en cambio mediante un add-on por asiento dedicado — consulta [Facturación de asientos de empleado](/guides/employee-seats). ## Los ocho dominios [#domains] El sistema se reparte en ocho dominios de API. Empieza por la guía de la tarea que tengas entre manos; cada una enlaza a sus endpoints en la Referencia de API. | Dominio | Qué hace | Guía | Scope | | -------------------------- | ------------------------------------------------------------ | -------------------------------------------- | ---------------------------------------------- | | Empleados | La plantilla: crear, editar, dar de baja, reactivar. | — | `employees:read` / `employees:write` | | Horarios | Horas semanales esperadas y asignaciones efectivo-datadas. | [Horarios](/guides/work-schedules) | `work_schedules:read` / `work_schedules:write` | | Fichajes | Entrada/salida, pausas, fichajes retroactivos, correcciones. | [Fichajes](/guides/time-clock) | `time_entries:read` / `time_entries:write` | | Cierres mensuales | Congelar, sellar, informar y exportar el registro. | [Cierre mensual](/guides/monthly-time-close) | `time_entries:read` / `time_entries:write` | | Exportaciones para nóminas | Fichero de incidencias para A3, Sage o NominaSOL. | [Cierre mensual](/guides/monthly-time-close) | `payroll_exports:read` | | Ausencias | Tipos, políticas, solicitudes, saldos y calendario. | [Ausencias](/guides/absences) | `absences:read` / `absences:write` | | Presencia | Quién trabaja ahora, en oficina o en remoto. | [Presencia](/guides/presence) | `presence:read` | | Festivos | Calendario nacional, autonómico y local por comunidad. | — | `holidays:read` | Dos dominios son de **solo lectura** en la API: **presencia** y **festivos** exponen únicamente lecturas (`presence:read`, `holidays:read`). Declarar la presencialidad oficina/remoto y crear festivos locales propios son tareas solo-portal — no existe el scope `presence:write` ni `holidays:write`. ## Los empleados y la plantilla [#employees] El empleado es la entidad ancla de la que depende el resto del sistema. Cada empleado lleva un nombre, un email único por empresa, un `tax_id` y un `job_title` opcionales, las horas semanales contratadas, una fecha de alta y la comunidad autónoma (`ccaa`) que determina qué festivos aplican. La baja es una **baja soft**: el empleado conserva su historial en el ledger (la retención de cuatro años prohíbe destruirlo) y puede reactivarse luego. ```bash curl -X POST https://api.factuarea.com/v1/employees \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Ana Ruiz", "email": "ana.ruiz@acme.example", "employment_type": "full_time", "contract_hours": 40, "hire_date": "2026-01-07", "ccaa": "ES-MD" }' ``` Consulta los esquemas completos del empleado en la [Referencia de API](/api-reference/employees/public-api.v1.employees.list). ## Scopes y MCP [#scopes] Cada dominio mapea a un scope fino del catálogo cerrado (`employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read`, `payroll_exports:read`), todos gateados tras el módulo `control_horario`. Revisa la lista completa en la [página de scopes](/guides/scopes-and-irreversibility) y en el [catálogo de scopes MCP](/mcp/scopes). Cada ruta v1 tiene su [tool MCP](/mcp/tools) espejo, así que un agente puede ejecutar las mismas operaciones. <Callout type="info"> El ledger de jornada son datos de cumplimiento, aislados por diseño: nunca referencia clientes, facturas ni proyectos. Responde a una sola pregunta — cuántas horas trabajó cada empleado — y mantiene esa evidencia intacta. </Callout> ## Por dónde seguir [#next] * [Fichajes](/guides/time-clock) — entrada/salida, pausas y el flujo de correcciones. * [Cierre mensual](/guides/monthly-time-close) — congelar, sellar y exportar el registro. * [Ausencias](/guides/absences) — tipos, políticas, solicitudes, saldos y arrastre. * [Horarios](/guides/work-schedules) — patrones semanales y asignaciones. * [Presencia](/guides/presence) — el panel de equipo en vivo y la vista diaria oficina/remoto. * [Facturación de asientos de empleado](/guides/employee-seats) — el add-on por asiento y su ciclo. --- # Resumen de MCP (/es/mcp) El **servidor MCP de Factuarea** expone la API pública como tools de [Model Context Protocol](https://modelcontextprotocol.io), de modo que los agentes de IA (Claude, ChatGPT, Cursor, tu propia app LLM) pueden leer y operar sobre tus datos de facturación a través de un único endpoint gobernado en lugar de escribir a mano llamadas HTTP. Habla el transporte **Streamable HTTP** y vive en: ``` https://mcp.factuarea.com ``` <Callout type="info"> El endpoint canónico es la raíz del subdominio. La forma anterior con path `https://mcp.factuarea.com/mcp` sigue funcionando como alias de compatibilidad, así que las configuraciones existentes siguen conectando. </Callout> Cada tool se corresponde con el mismo contrato `https://api.factuarea.com/v1` documentado en la Referencia de la API: recursos idénticos, el mismo `id` opaco (UUID v7), los mismos errores normalizados, el mismo aislamiento multi-tenant por empresa. La capa MCP añade descubrimiento (`tools/list`), aplicación de **scope** por tool y un flujo de consentimiento para apps de terceros. <Cards> <Card icon="<Boxes />" title="Instala el plugin de Claude Code" href="/mcp/claude-code-plugin"> La configuración recomendada — dos comandos instalan el plugin oficial `factuarea-mcp` y conectan Claude Code sobre OAuth. </Card> <Card icon="<Bot />" title="Conecta cualquier cliente" href="/mcp/connect"> Configura Claude Desktop, el MCP Inspector o cualquier cliente MCP manualmente, usando OAuth o una API key. </Card> <Card icon="<Wrench />" title="<>Explora las <Stat n="tools" /> tools</>" href="/mcp/tools"> El catálogo completo agrupado por dominio, con el scope que requiere cada tool. </Card> </Cards> ## Qué puede hacer [#qué-puede-hacer] El servidor publica **<Stat n="tools" /> tools** en 27 dominios. Todo lo que puedes hacer con la REST API puedes hacerlo aquí, en el formato nativo de tool-calling del agente: <Cards> <Card icon="<Boxes />" title="Ventas y compras" href="/mcp/tools#invoice"> Facturas, presupuestos, facturas proforma, albaranes, facturas recurrentes y facturas de compra — crear, actualizar, transicionar, enviar y generar PDFs. </Card> <Card icon="<Code />" title="Catálogo y CRM" href="/mcp/tools#client"> Clientes, proveedores, productos, series de numeración y tipos impositivos. </Card> <Card icon="<ShieldCheck />" title="Cumplimiento" href="/mcp/tools#verifactu"> Registros, eventos y certificados de VeriFactu (AEAT), envíos a FACe (FacturaE), además de webhooks y el catálogo de eventos. </Card> <Card icon="<Clock />" title="Control horario" href="/mcp/tools#employee"> Empleados, horarios de trabajo, el registro de fichajes, cierres mensuales, ausencias, presencia y festivos — el registro del RD-ley 8/2019. </Card> </Cards> ## Cuándo usar MCP, REST o SDKs [#cuándo-usar-mcp-rest-o-sdks] El servidor MCP, la REST API y los SDKs oficiales son tres puertas de entrada al **mismo** backend. Elige según quién (o qué) está llamando: | Estás construyendo… | Usa | Por qué | | ------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Un **agente / asistente de IA** que razona sobre tus datos y actúa sobre ellos | **Servidor MCP** | Las tools se autodescriben; el modelo las descubre y las llama sin que cablees cada endpoint. Los scopes y el consentimiento se aplican en cada llamada. | | Un **servicio backend, cron job o integración** con lógica fija | **REST API** | Determinista, sin modelo en el bucle, control total sobre las peticiones y los reintentos. | | Un **cliente tipado** en tu app (TypeScript o PHP) | [**SDKs oficiales**](/sdks) | `@factuarea/sdk` y `factuarea/factuarea-php` envuelven la REST API con tipos, reintentos y helpers de idempotencia. | <Callout type="info"> Las tres superficies comparten los mismos identificadores, envoltorio de error y scopes, así que puedes combinarlas: prototipa un flujo con un agente sobre MCP y luego endurece el camino crítico como una integración REST o SDK. </Callout> ## Dos formas de autenticarse [#dos-formas-de-autenticarse] El mismo endpoint `/mcp` acepta dos tipos de credencial, para dos audiencias distintas: | Canal | Credencial | Para | Tools alcanzables | | ------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **API key** | Bearer token `fact_live_…` / `fact_test_…` | **El titular de la cuenta** automatizando su propia empresa (como un PAT de GitHub) | Hasta **<Stat n="tools" />** — concedes los scopes que quieras, incluido `*` | | **OAuth 2.1** | Access token emitido vía consentimiento | **Apps de terceros** actuando en nombre de un usuario | **<Stat n="oauth_reachable" />** — un catálogo curado que excluye las escrituras de VeriFactu, el borrado RGPD, FacturaE (FACe), Pagos y pasarelas, y las tools de gestoría y escritura de cuenta | Consulta [Conectar un cliente](/mcp/connect#channel-policy) para la política completa de canales, y [Scopes y permisos](/mcp/scopes) para el catálogo de scopes. ## Construye primero en modo de prueba [#construye-primero-en-modo-de-prueba] Igual que en la REST API, una clave `fact_test_` — o un consentimiento OAuth con el entorno **Test** seleccionado — opera sobre una **empresa sandbox** aislada con los efectos externos desactivados (sin transmisión a AEAT, sin emails reales, sin webhooks salientes). Construye y valida contra test, luego cambia a producción. Consulta [Modo de prueba](/mcp/connect#test-mode). <Callout type="info"> El servidor MCP está **incluido en todos los planes de Factuarea**, junto con el resto de la API pública. Crea una API key desde [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) (o conéctate por OAuth) y empieza a llamar tools. </Callout> --- # Plugin de Claude Code (/es/mcp/claude-code-plugin) El marketplace de Factuarea publica **dos** plugins de Claude Code, para dos trabajos distintos. **`factuarea-mcp`** es la forma más rápida de conectar [Claude Code](https://claude.com/claude-code) al servidor MCP de Factuarea: registra el servidor (`https://mcp.factuarea.com`) e incluye una skill que enseña a Claude a usar bien las tools — scopes, paginación por cursor, el envoltorio de error y el modo de prueba — para que no tengas que configurar nada a mano. **`factuarea-api`** sirve a la otra audiencia, quien escribe el código de la integración, y a propósito **no declara ningún servidor MCP**. <Callout type="info"> Esta es la forma **recomendada** de conectar Claude Code. ¿Prefieres configurar el servidor manualmente (otros clientes, entornos headless)? Consulta [Conectar un cliente](/mcp/connect). </Callout> ## Instalación [#instalación] <Steps> <Step> **Añade el marketplace** Registra el catálogo de plugins de Factuarea. Ejecuta esto dentro de Claude Code: ```text /plugin marketplace add factuarea/claude-plugins ``` </Step> <Step> **Instala el plugin que necesites** ```text /plugin install factuarea-mcp@factuarea ``` Claude Code instala el plugin y registra el servidor MCP `factuarea`. ¿También vas a escribir código de integración? Añade además [`factuarea-api`](#integrator-skills): los dos son complementarios. </Step> </Steps> Para obtener actualizaciones más adelante, ejecuta `/plugin marketplace update factuarea`. ## Conecta el servidor [#conecta-el-servidor] El plugin declara el servidor **sin cabecera de auth**, así que la ruta recomendada es OAuth — nunca se pega nada secreto en un archivo de configuración. <Steps> <Step> **Autentícate** ```text /mcp ``` Elige **factuarea** y selecciona **Authenticate**. Tu navegador abre la pantalla de consentimiento de Factuarea. El [Dynamic Client Registration](/mcp/connect#oauth-21) y PKCE ocurren automáticamente — no hay client id ni secret que pegar. </Step> <Step> **Aprueba** En la pantalla de consentimiento seleccionas la **empresa**, el **entorno** (producción o prueba) y los **scopes** que concedes. Los scopes sensibles (borrados, `invoices:void`) están marcados y no vienen premarcados. Claude Code almacena el token y lo refresca de forma transparente. </Step> <Step> **Úsalo** Pide a Claude que trabaje con tus datos de Factuarea — "lista las facturas impagadas de este trimestre en modo de prueba", "crea un borrador de factura para Acme S.L.", "comprueba la cadena VeriFactu". La skill se carga automáticamente; también puedes invocarla explícitamente: ```text /factuarea-mcp:factuarea-mcp ``` </Step> </Steps> ### Conectar con una API key en su lugar [#conectar-con-una-api-key-en-su-lugar] Para entornos headless, o cuando ya tienes una clave `fact_`, conecta con una cabecera estática en lugar de OAuth: ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com \ --header "Authorization: Bearer fact_live_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Usa una clave `fact_test_` para apuntar al [sandbox](/mcp/connect#test-mode) aislado. La superficie de la API es idéntica — solo el prefijo cambia el entorno. Con una cabecera de clave **no** necesitas el flujo OAuth; la clave autentica cada petición. ## Qué incluye `factuarea-mcp` [#qué-incluye-factuarea-mcp] <Cards> <Card icon="<Wrench />" title="El servidor MCP" href="/mcp/tools"> La declaración del servidor `factuarea` (`https://mcp.factuarea.com`, transporte HTTP), para que Claude pueda llamar directamente a todas las tools de Factuarea. </Card> <Card icon="<BookOpen />" title="Una skill de guía" href="/mcp/scopes"> Una skill que da a Claude el contexto para usar bien las tools — la política de canal, los dominios de tools y sus scopes, la identidad UUID v7, la paginación por cursor, el envoltorio de error y el modo de prueba. </Card> <Card icon="<Code />" title="El plugin factuarea-api" href="#integrator-skills"> Una instalación aparte y más ligera para escribir la propia integración — cinco skills, y ninguna declaración de servidor MCP: ni OAuth ni tools cargadas. </Card> </Cards> La skill de guía conoce la **política de canal** (una API key alcanza las <Stat n="tools" /> tools; OAuth usa las <Stat n="oauth_reachable" /> curadas, sin conceder nunca `verifactu:write`, los scopes de FacturaE, Pagos ni gestoría/escritura de cuenta, ni la operación GDPR de olvido de firma a apps de terceros), cómo el plan/módulo y los feature flags acotan aún más `tools/list`, y que los cambios de estado son **tools discretas** (`mark_invoice_as_paid`, `void_invoice`, `accept_quote`…), no un genérico `change_status`. ## Construir la integración: el plugin `factuarea-api` [#integrator-skills] El plugin anterior sirve para **operar tu cuenta** mediante tools MCP. Un segundo plugin cubre el trabajo contrario: **escribir el código** que llama a la REST API desde tu propio backend: ```text /plugin install factuarea-api@factuarea ``` **No declara ningún servidor MCP**, y eso es lo que lo hace barato de tener instalado: ni consentimiento OAuth ni superficie de tools cargada en la sesión. Sus cinco skills se cargan según la tarea que tengas entre manos y se apoyan en los SDKs oficiales, en la especificación viva y en esta documentación. | Skill | Se carga cuando la tarea es… | Qué cubre | | ------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`factuarea-api`** | Empezar, o preguntar qué admite la API, cómo funciona la autenticación o qué dice la documentación | El punto de entrada: diez reglas de oro, las dos cabeceras de autenticación admitidas y el prefijo que decide el entorno, búsqueda local en esta documentación con [`factuarea docs`](/cli/usage), y recetas que enrutan a las cuatro skills siguientes | | **`factuarea-implement`** | Montar el cliente y hacer las primeras llamadas | Elegir entre el SDK de [TypeScript](/sdks/typescript) y el de [PHP](/sdks/php), resolver la clave desde el entorno, el envoltorio `data`, la [paginación por cursor](/guides/pagination), el [`Idempotency-Key`](/guides/idempotency) en las escrituras y empezar en el sandbox | | **`factuarea-webhooks`** | Escribir o arreglar el endpoint que recibe las entregas | Verificación HMAC de `Factuarea-Signature` sobre el cuerpo crudo, comparación en tiempo constante, deduplicación por `Factuarea-Event-Id`, un 2xx rápido con el trabajo pesado diferido, la ventana de gracia de la rotación y las pruebas en local con `factuarea listen` | | **`factuarea-audit`** | Revisar una integración que ya existe | Seis familias de reglas — verificación de firma, idempotencia en las escrituras, exposición de la API key, manejo de errores por `code`, límites de peticiones y ciclo de vida del documento — reportando cada hallazgo con una severidad, un `fichero:línea` y la corrección concreta | | **`factuarea-upgrade`** | Realinear tras un cambio de contrato o de SDK | Desalineación entre el código y la especificación viva, la versión fijada del SDK frente a la última publicada, y un informe que separa los cambios que rompen de los aditivos, en el orden en que aplicarlos | <Callout type="info"> Los dos plugins son complementarios, no alternativas. `factuarea-mcp` lee y actúa sobre tus datos mediante tools; `factuarea-api` nunca llama a la API por ti — escribe y revisa el código que sí lo hace. Los equipos que construyen una integración suelen instalar ambos. También puedes generar tú mismo un cliente a partir de la [especificación OpenAPI](/api/openapi). </Callout> ## Resolución de problemas [#resolución-de-problemas] | Síntoma | Causa | Solución | | ------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Una tool devuelve `401`** | No estás autenticado, o la clave/token caducó. | Ejecuta `/mcp` → **factuarea** → **Authenticate** para (re)iniciar OAuth, o revisa tu cabecera de API key. | | **`insufficient_scope` (`403`)** | La credencial carece del scope que requiere la tool. | Vuelve a autenticarte y aprueba el scope, o usa una clave que lo tenga. Recuerda que `verifactu:write` y la tool de olvido de firma son **solo para API key**. | | **Una tool que esperabas no aparece** | `tools/list` se filtra por tus scopes y feature flags. | Concede el scope (o usa una clave más amplia); confirma que el canal de la credencial puede alcanzarla (OAuth excluye las tools de solo API key). Esto es lo esperado, no un bug. | | **`addon_not_active` (`-32007`)** | La empresa no tiene un plan de Factuarea activo que incluya acceso a la API (p. ej. un trial caducado). | Contrata o renueva un plan desde el dashboard; toda la superficie MCP requiere un plan activo. | | **`429` con `Retry-After`** | Se alcanzó un bucket de [límite de peticiones](/mcp/errors#rate-limits). | Espera los segundos de `Retry-After` antes de reintentar — no insistas sin parar. | Consulta [Errores y límites de peticiones](/mcp/errors) para la tabla completa de códigos. --- # Conectar un cliente (/es/mcp/connect) El servidor MCP de Factuarea habla **Streamable HTTP** en `https://mcp.factuarea.com`. Cualquier cliente compatible con MCP puede conectarse usando una de las dos credenciales admitidas: * **OAuth 2.1** — el cliente se registra a sí mismo y el usuario lo autoriza mediante una pantalla de consentimiento. Ideal para herramientas de usuario final. * **API key** — pasas un Bearer token `fact_live_` / `fact_test_` directamente. Ideal para tus propias automatizaciones. <Callout type="info"> El endpoint canónico es la raíz del subdominio, `https://mcp.factuarea.com`. La forma anterior con ruta `https://mcp.factuarea.com/mcp` sigue funcionando como alias de compatibilidad. </Callout> <Callout type="info"> **¿Usas Claude Code?** La configuración recomendada es el plugin oficial `factuarea-mcp` — dos comandos y estás conectado por OAuth, con una skill de orientación incluida. Consulta la guía dedicada del [plugin de Claude Code](/mcp/claude-code-plugin). Los pasos manuales de abajo son para otros clientes o configuraciones headless. </Callout> ## Claude Code [#claude-code] El camino más sencillo es el [plugin de Claude Code](/mcp/claude-code-plugin) — registra el servidor e incluye una skill de orientación en una sola instalación. Si prefieres conectar el servidor a mano, [Claude Code](https://claude.com/claude-code) también admite servidores MCP remotos sobre HTTP con OAuth integrado. ### Con OAuth [#con-oauth] <Steps> <Step> **Añade el servidor** ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com ``` </Step> <Step> **Autentícate** Dentro de Claude Code, ejecuta el slash command: ```text /mcp ``` Elige **factuarea**, selecciona **Authenticate** y tu navegador abre la pantalla de consentimiento. Selecciona la **empresa** a la que dar acceso, el **entorno** (live o test) y los **scopes** que quieras permitir, y luego confirma. Claude Code almacena el token resultante y lo renueva automáticamente. </Step> <Step> **Úsalo** Pide a Claude que haga algo — "lista mis facturas vencidas en modo de prueba" — y descubre y llama a las tools correspondientes. </Step> </Steps> ### Con una API key [#con-una-api-key] Si prefieres usar tu propia clave (sin flujo de consentimiento), pásala como un header `Authorization`: ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com \ --header "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Los scopes de la clave determinan qué tools aparecen en `tools/list`. Una clave con `*` ve las <Stat n="tools" /> tools; una clave más restringida solo ve las tools que cubren sus scopes. ## Claude Desktop [#claude-desktop] [Claude Desktop](https://claude.com/download) se conecta a servidores remotos mediante su archivo de configuración. Añade una entrada bajo `mcpServers`: ```json { "mcpServers": { "factuarea": { "type": "http", "url": "https://mcp.factuarea.com" } } } ``` En el siguiente arranque, Claude Desktop descubre el servidor y te guía por el flujo de consentimiento OAuth en tu navegador. Para usar una API key en su lugar, añade un objeto `headers`: ```json { "mcpServers": { "factuarea": { "type": "http", "url": "https://mcp.factuarea.com", "headers": { "Authorization": "Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` <Callout type="info"> El archivo de configuración está en `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Reinicia la app después de editarlo. </Callout> ## MCP Inspector [#mcp-inspector] El [MCP Inspector](https://github.com/modelcontextprotocol/inspector) es la forma más rápida de explorar el catálogo y llamar a tools a mano mientras desarrollas. <Steps> <Step> **Arráncalo** ```bash npx @modelcontextprotocol/inspector ``` </Step> <Step> **Conecta** Configura **Transport** a `Streamable HTTP` y **URL** a `https://mcp.factuarea.com`. Para OAuth, el Inspector ejecuta el flujo de autorización por ti. Para una API key, añade un header `Authorization: Bearer fact_test_…` bajo **Authentication**. </Step> <Step> **Explora** Abre **Tools → List Tools** para ver todas las tools que tu credencial puede alcanzar, inspecciona su esquema de entrada y ejecútala con argumentos de ejemplo. </Step> </Steps> ## Cualquier cliente MCP [#cualquier-cliente-mcp] El servidor sigue la especificación MCP, así que cualquier cliente conforme funciona. Lo esencial: * **Endpoint** — `https://mcp.factuarea.com` (la forma con ruta `…/mcp` es un alias de compatibilidad) * **Transport** — Streamable HTTP * **Auth** — `Authorization: Bearer <token>`, donde el token es una API key (`fact_live_` / `fact_test_`) o un access token de OAuth 2.1 * **Discovery** — ante un `401`, el servidor devuelve un header `WWW-Authenticate` que apunta a sus [Protected Resource Metadata](#discovery) (RFC 9728) para que los clientes encuentren el authorization server automáticamente El descubrimiento de tools está paginado; el servidor devuelve el catálogo completo (hasta 200 tools) en una sola respuesta `tools/list` por defecto, y respeta `nextCursor` si tu cliente pagina. ## Autenticación [#authenticate] El servidor MCP acepta dos tipos de credencial en el mismo endpoint `https://mcp.factuarea.com`, para dos audiencias distintas. Ambas llegan como `Authorization: Bearer <token>`; el servidor las distingue por la forma del token (`fact_*` → API key, cualquier otra cosa → access token de OAuth). ### Política de canal [#channel-policy] Esta es la regla más importante de la superficie MCP: | Canal | Quién | Cómo se eligen los scopes | Tools accesibles | | ------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------- | | **API key** | El **propietario de la cuenta** automatizando su propia empresa | Eliges los scopes al crear la clave — hasta el super-scope `*` | **<Stat n="tools" />** (todo) | | **OAuth 2.1** | Una **app de terceros** actuando en nombre de un usuario | El usuario concede los scopes en la pantalla de consentimiento, desde un **catálogo curado** | **<Stat n="oauth_reachable" />** | El modelo refleja el de GitHub: un **personal access token** (API key) es la credencial propia del propietario y puede tener cualquier permiso, mientras que una **OAuth app** es externa y se limita a un conjunto verificado de scopes que el usuario aprueba explícitamente. Las <Stat n="oauth_restricted" /> tools que una OAuth app **nunca** puede alcanzar (solo una API key puede) son las operaciones fiscales y de privacidad más sensibles, además de la superficie first-party de gestión de la cuenta: * **Escrituras de VeriFactu** (`verifactu:write`) — 8 tools: registrar/reintentar/subsanar registros y eventos de VeriFactu, subir/activar/revocar certificados FNMT, actualizar ajustes de VeriFactu. Tocan el cumplimiento de la AEAT y son exclusivas del propietario. * **Borrado RGPD** (`delivery_notes:gdpr_forget`) — 1 tool: borrar la PII de auditoría de firma (Art. 17). Privilegiada, solo para administradores. * **FacturaE (FACe)** (`facturae:read` / `facturae:write`) — 5 tools: las operaciones B2G de FACe. Sus scopes aún no están en el catálogo de consentimiento OAuth, así que por ahora son solo para API key. * **Pagos y pasarelas** (`stripe_autoinvoicing:*`, `payouts:read`) — 10 tools: configuración de auto-facturación de Stripe Connect, cuentas conectadas y payouts. Scopes granulares, solo API key, sin equivalente de consentimiento OAuth. * **Gestoría** (`companies:*`, `api_keys:*`) — 16 tools: gestión de las empresas hijas y sus API keys. Gestionar sub-cuentas y credenciales es first-party, nunca se concede por consentimiento de terceros. * **Escrituras de cuenta** (`account:write`) — 4 tools: crear/rotar/revocar tus propias API keys y actualizar la personalización de la cuenta. Solo first-party. Todo lo demás — las <Stat n="oauth_reachable" /> tools de lectura/escritura/transición/envío — está disponible para ambos canales. Consulta [Scopes & permisos](/mcp/scopes) para ver el catálogo. ### API keys [#api-keys] Una API key es un Bearer token opaco vinculado a tu empresa, creado en el dashboard de desarrolladores en [Ajustes → Desarrolladores → API Keys](https://app.factuarea.com/settings/developers/api-keys). El formato y las reglas son idénticos a los de la API REST: ``` fact_live_<24 alphanumeric characters> → production company fact_test_<24 alphanumeric characters> → isolated sandbox company ``` El prefijo es la fuente de verdad del **entorno** — consulta [Modo de prueba](#test-mode). El secreto se muestra **solo una vez** en la creación; el backend solo almacena un hash bcrypt. Pásalo a tu cliente MCP como: ``` Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` Para el ciclo de vida completo de la clave — creación, scopes, rotación con periodo de gracia, revocación, lista de acceso de IPs, `expires_at` — consulta la [guía de Autenticación](/guides/authentication) canónica. Las claves se comparten entre las superficies REST y MCP. ### OAuth 2.1 [#oauth-21] Para apps de terceros, Factuarea es un **OAuth 2.1 Authorization Server** completo. Es compatible con Dynamic Client Registration, el flujo de authorization-code con PKCE y la rotación de refresh-token. No se requiere pre-registro ni aprobación manual de la app — un cliente se registra a sí mismo y el usuario lo autoriza. (Los clientes interactivos como Claude Code y el MCP Inspector ejecutan todo este flujo por ti; los pasos de abajo son para construir tu propio cliente.) #### Descubrimiento [#discovery] Los clientes descubren las capacidades del servidor a través de endpoints de metadata estándar (sin prefijo `/api`): | Endpoint | RFC | Propósito | | ----------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/.well-known/oauth-authorization-server` | [8414](https://www.rfc-editor.org/rfc/rfc8414) | Authorization Server Metadata — lista los endpoints authorize/token/register/introspect/revoke, los scopes admitidos, `code_challenge_methods_supported: ["S256"]`. | | `/.well-known/oauth-protected-resource` | [9728](https://www.rfc-editor.org/rfc/rfc9728) | Protected Resource Metadata — declara el recurso MCP y qué authorization server emite tokens válidos. | Cuando una petición no autenticada alcanza el endpoint, el servidor responde `401` con un header `WWW-Authenticate: Bearer ..., resource_metadata="<url>"` para que los clientes RFC 9728 encuentren el authorization server sin adivinar. #### 1. Dynamic Client Registration (RFC 7591) [#1-dynamic-client-registration-rfc-7591] Un cliente se registra a sí mismo haciendo POST de su metadata; el servidor devuelve un `client_id` (y un `client_secret` para clientes confidenciales): ```bash curl -X POST https://mcp.factuarea.com/api/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "My Invoicing Assistant", "redirect_uris": ["https://myapp.example.com/callback"], "token_endpoint_auth_method": "none" }' ``` Los clientes públicos (apps de navegador/nativas) se registran con `token_endpoint_auth_method: "none"` y dependen de PKCE; los clientes confidenciales usan `client_secret_basic`. El registro tiene un rate limit de **60 por minuto por IP**. #### 2. Autorización con PKCE [#2-autorización-con-pkce] Envía al usuario al endpoint authorize con un challenge PKCE (`code_challenge_method=S256` es el único método aceptado): ``` GET https://mcp.factuarea.com/api/oauth/authorize ?response_type=code &client_id=<client_id> &redirect_uri=https://myapp.example.com/callback &scope=factuarea.read invoices.write &state=<opaque> &code_challenge=<base64url(sha256(verifier))> &code_challenge_method=S256 ``` Esto renderiza la **pantalla de consentimiento**, donde el usuario: 1. Elige la **empresa** a la que dar acceso (un usuario puede pertenecer a varias). 2. Elige el **entorno** — **live** (la empresa real) o **test** (un sandbox aislado), al estilo Stripe. Test es opt-in; si está ausente ⇒ live. 3. Revisa y **selecciona los scopes** a conceder. Los scopes sensibles se marcan y no vienen pre-seleccionados. Al aprobar, el servidor redirige de vuelta con un `code` de un solo uso (y tu `state`). Las operaciones sensibles se filtran del catálogo que el usuario puede aprobar — consulta la [política de canal](#channel-policy). #### 3. Intercambio de token [#3-intercambio-de-token] Intercambia el code por un access token, enviando el verificador PKCE: ```bash curl -X POST https://mcp.factuarea.com/api/oauth/token \ -d grant_type=authorization_code \ -d code=<code> \ -d redirect_uri=https://myapp.example.com/callback \ -d client_id=<client_id> \ -d code_verifier=<verifier> ``` ```json { "access_token": "<opaque>", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "<opaque>", "scope": "profile.read clients.read invoices.read invoices.write" } ``` El token persiste los scopes **expandidos y de grano fino** (las macros como `factuarea.read` se expanden en el momento de la emisión). Usa el access token como credencial Bearer. El endpoint token tiene un rate limit de **60 por minuto por (cliente, IP)** y requiere autenticación de cliente (HTTP Basic para clientes confidenciales, `client_id` en el body para los públicos). #### 4. Rotación de refresh-token [#4-rotación-de-refresh-token] Los refresh tokens **rotan por familia**: cada refresh emite un nuevo access token y un nuevo refresh token, e invalida el que usaste. ```bash curl -X POST https://mcp.factuarea.com/api/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=<refresh_token> \ -d client_id=<client_id> ``` Si un refresh token se **reutiliza** (usado tras la rotación — la señal clásica de una fuga), el servidor detecta la reutilización, revoca toda la familia de tokens y lanza una alerta de seguridad. Almacena y usa siempre solo el último refresh token. #### Revocación e introspección [#revocación-e-introspección] | Endpoint | RFC | Propósito | | ---------------------------- | ---------------------------------------------- | ------------------------------------------------------------ | | `POST /api/oauth/revoke` | [7009](https://www.rfc-editor.org/rfc/rfc7009) | Revoca un access o refresh token. | | `POST /api/oauth/introspect` | [7662](https://www.rfc-editor.org/rfc/rfc7662) | Comprueba si un token está activo y lee sus scopes/metadata. | Ambos requieren autenticación de cliente. Los usuarios también pueden revisar y revocar apps conectadas desde el dashboard de Factuarea, y a un administrador de empresa que pierde el acceso se le revocan sus tokens automáticamente en la siguiente llamada. ## Modo de prueba [#test-mode] El servidor MCP funciona contra los mismos dos **entornos** que la API REST — **live** (tu empresa real) y **test** (un sandbox aislado) — para que puedas construir y validar una integración de agente sin tocar datos de producción, la AEAT ni las bandejas de entrada de tus clientes. Cómo seleccionas el modo de prueba depende del canal: | Canal | Cómo usar el modo de prueba | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **API key** | Autentícate con una clave `fact_test_`. El prefijo es la fuente de verdad — un token `fact_test_` siempre opera sobre el sandbox. | | **OAuth 2.1** | En la pantalla de consentimiento, elige el entorno **Test** (al estilo Stripe). Si está ausente ⇒ **live**. El token emitido queda vinculado a ese entorno. | Una credencial de test opera sobre una **empresa sandbox** dedicada — un gemelo técnico de tu empresa real, aprovisionado automáticamente y que hereda su plan, para que el gating de módulo/plan se comporte fielmente. El aislamiento es **estructural** (los datos de test y live viven en empresas separadas), y los efectos externos están desactivados: | Efecto | En `live` | En `test` | | ------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **VeriFactu** | El registro de Alta se crea y se transmite a la AEAT. | Se crea **localmente**, pero **nunca se transmite** a la AEAT. | | **Email** | Los emails de documentos llegan a los destinatarios reales. | **No se entregan** a los destinatarios reales. | | **Webhooks** | Los eventos suscritos se entregan a tus endpoints. | Se registran con `livemode: false`, pero **no se entregan**. | | **FACe (FacturaE)** | Los envíos se presentan al web service real de FACe. | **Simulados** — ninguna llamada SOAP sale de Factuarea; el número de registro es sintético (`FACE-SANDBOX-*`). | Todo lo demás se comporta exactamente igual que en producción, y el conjunto completo de <Stat n="tools" /> tools está disponible en ambos entornos (sujeto a tus scopes y plan). Cuando tu flujo funcione de extremo a extremo, cambia a live: crea una clave `fact_live_`, o vuelve a ejecutar el flujo de consentimiento y selecciona el entorno **live**. <Callout type="info"> Este es el mismo mecanismo de sandbox que la API REST. Consulta la guía canónica [Modo de prueba & sandbox](/guides/test-mode) para saber cómo se aprovisiona y consulta la empresa sandbox. </Callout> --- # Errores y límites de peticiones (/es/mcp/errors) El servidor MCP habla **JSON-RPC 2.0** estricto. Los fallos vuelven como un objeto `error`, nunca como un cuerpo de error HTTP al estilo de la API REST — pero la **semántica es idéntica**: la misma violación de regla de negocio que devuelve `422` sobre REST devuelve aquí el error JSON-RPC equivalente, con el `code` y el `http_status` de v1 conservados en `data`. Esta página cubre el mapeo JSON-RPC específico de MCP y los buckets de throttling de MCP. Para el contrato REST canónico — el envoltorio de error por `code` y las cuotas por tier — consulta [Errores](/guides/errors) y [Límites de peticiones](/guides/rate-limits). ## Forma del error [#forma-del-error] ```json { "jsonrpc": "2.0", "id": "<request id>", "error": { "code": -32008, "message": "invoice_cannot_be_modified", "data": { "http_status": 422, "code": "invoice_cannot_be_modified", "hint": "La factura ya emitida no puede modificarse.", "param": "status" } } } ``` * **`error.code`** — el código numérico JSON-RPC (siempre en el rango `-32099..-32000` definido por la implementación, o `-32603` para errores internos). * **`error.message`** — un identificador de cadena estable (p. ej. `insufficient_scope`, `invoice_cannot_be_modified`). * **`error.data.code`** — el mismo `code` canónico de v1 que devuelve la API REST, para que puedas ramificar según un único valor en ambas superficies. * **`error.data.http_status`** — el estado HTTP que devolvería la llamada REST equivalente (422 / 404 / 409 / …), para clientes que prefieren razonar en términos HTTP. * **`error.data.hint`** — un mensaje legible para personas (en español, acorde con el idioma de la app). Otros campos (`param`, `subcode`, `required_scope`, …) aparecen cuando son relevantes. ## Tabla de códigos [#tabla-de-códigos] | JSON-RPC code | `message` / `data.code` | HTTP equiv. | Significado | | ------------- | --------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-32001` | `invalid_token` | 401 | Credencial ausente, malformada o desconocida; o el usuario ya no es miembro de la empresa. | | `-32002` | `client_revoked` | 403 | El cliente OAuth fue revocado. | | `-32003` | `invalid_token_type` | 403 | Tipo de credencial incorrecto para esta superficie. | | `-32004` | `plan_limit_exceeded` / `plan_upgrade_required` | 402 | Se alcanzó un límite de uso del plan, o la acción requiere un plan superior. `data` incluye `resource`, `current`, `limit`. | | `-32005` | `insufficient_scope` / `module_not_in_plan` / `feature_flag_disabled` | 403 | La credencial carece del scope requerido, el módulo no está en el plan, o un feature flag está desactivado. `data` incluye `required_scope` / `module` / `flag`. | | `-32006` | `rate_limit_exceeded` | 429 | Se superó un bucket de throttling. `data` incluye `retry_after` y `bucket`; la respuesta también establece la cabecera `Retry-After`. | | `-32007` | `addon_not_active` | 403 | La empresa no tiene un plan de Factuarea activo que incluya acceso a la API pública (p. ej. un trial caducado o una suscripción vencida fuera de su periodo de gracia). | | `-32008` | *(código v1)* | 422 / 404 / 409 / … | Una violación de regla de negocio, recurso inexistente o conflicto. `message` y `data.code` son el código de error canónico de v1; `data.http_status` te indica la categoría. | | `-32603` | `internal_error` | 500 | Error inesperado del servidor. | <Callout type="info"> `-32005` y `-32008` cubren cada uno varias subcausas. Ramifica siempre según `data.code` (la cadena), no solo según el `code` numérico, cuando necesites distinguirlas — p. ej. `insufficient_scope` y `module_not_in_plan` afloran ambos como `-32005`. </Callout> ### Scope insuficiente [#scope-insuficiente] Cuando una tool necesita un scope que la credencial no tiene: ```json { "jsonrpc": "2.0", "id": "req-42", "error": { "code": -32005, "message": "insufficient_scope", "data": { "http_status": 403, "code": "insufficient_scope", "required_scope": "invoices:write", "provided_scopes": ["invoices:read", "clients:read"], "hint": "La credencial no tiene el scope requerido para esta operación." } } } ``` Las tools que tu credencial no puede alcanzar también quedan **ocultas** en `tools/list`, así que un agente bien comportado normalmente no las intentará — este error es la red de seguridad. ## Límites de peticiones [#límites-de-peticiones] Las peticiones se limitan en tres buckets independientes. Superar cualquiera de ellos devuelve `-32006` con una cabecera `Retry-After` (segundos). ### Por token, por categoría de tool [#por-token-por-categoría-de-tool] Cada credencial tiene contadores por minuto separados por **categoría** de tool, de modo que un uso destructivo intensivo no pueda agotar tus lecturas: | Categoría | Límite por defecto | Tools de ejemplo | | ---------------- | ------------------ | --------------------------------------- | | `read` / `write` | 60 / min | `search_invoices`, `create_invoice` | | `send` | 20 / min | `send_invoice`, `send_quote` | | `generate` | 30 / min | `get_invoice_facturae_link` | | `destructive` | 10 / min | `delete_invoice`, `bulk_delete_clients` | El bucket se resuelve a partir de la [categoría](/mcp/tools#how-to-read-this-catalog) de la tool. El contador se incrementa **antes** de que la tool se ejecute, así que las llamadas rechazadas (scope incorrecto, error de validación) también consumen cuota — esto es anti-abuso deliberado, acorde con el patrón estándar OAuth/REST. ### Por plan, cada hora [#por-plan-cada-hora] Un tope horario a nivel de empresa por slug de plan. El plan **Enterprise** se salta este bucket por completo. ### Por cliente OAuth, global [#por-cliente-oauth-global] Las apps OAuth comparten además un bucket global por cliente de **1000 / min**, de modo que una sola app que se porte mal no pueda saturar el servidor a través de todos sus usuarios. ### Cabeceras de límite de peticiones [#cabeceras-de-límite-de-peticiones] Las respuestas correctas llevan el presupuesto restante para que puedas reducir el ritmo de forma proactiva: | Header | Significado | | --------------------------------------------------------- | ---------------------------------------------------- | | `X-RateLimit-Limit-Token` / `X-RateLimit-Remaining-Token` | El bucket por token (por categoría). | | `X-RateLimit-Limit-Hour` / `X-RateLimit-Remaining-Hour` | El bucket por plan horario (ausente en Enterprise). | | `Retry-After` | En un `429`, segundos a esperar antes de reintentar. | ### Límites de los endpoints de auth [#límites-de-los-endpoints-de-auth] Los endpoints OAuth tienen sus propios límites, independientes de los buckets de MCP: | Endpoint | Límite | | -------------------------- | -------------------------- | | `POST /api/oauth/register` | 10 / hora por IP | | `POST /api/oauth/token` | 60 / min por (cliente, IP) | <Callout type="warn"> Respeta siempre `Retry-After`. Reintentar antes de que transcurra mantiene el bucket lleno y solo retrasa tu recuperación. Combínalo con idempotencia en las escrituras para que un reintento retrasado nunca duplique un documento. </Callout> --- # Scopes y permisos (/es/mcp/scopes) Cada tool MCP declara el **scope** que una credencial debe tener para invocarla. Los scopes funcionan de forma ligeramente distinta según el canal: * Las **API keys** se crean directamente con scopes **detallados** (`resource:action`, p. ej. `invoices:read`) — el mismo catálogo cerrado que usa la API REST. También puedes conceder el super-scope `*`. * Los **tokens OAuth** reciben scopes con **punto** (`resource.action`, p. ej. `invoices.read`) en la pantalla de consentimiento. El servidor los traduce a los scopes detallados automáticamente, de modo que ambos canales aplican el mismo conjunto en el límite de la tool. ## Catálogo de consentimiento OAuth [#catálogo-de-consentimiento-oauth] Estos son los scopes que un usuario puede conceder a una app de terceros en la pantalla de consentimiento. Hay **59 scopes simples** más **3 macros**. ### Scopes simples [#scopes-simples] Cada uno concede una capacidad. La columna **Maps to** muestra el scope detallado que aplican las tools — la capa de consentimiento traduce los scopes OAuth con punto a estos automáticamente. La columna **Sensitive** marca los scopes que la pantalla de consentimiento destaca y no marca por defecto. #### Perfil [#perfil] | Scope | Concede | Maps to | Sensitive | | -------------- | --------------------------------------- | -------------- | --------- | | `profile.read` | Leer tu nombre, email y empresa activa. | `account:read` | no | #### CRM — clientes y proveedores [#crm--clientes-y-proveedores] | Scope | Concede | Maps to | Sensitive | | ------------------ | ------------------------------- | ------------------ | --------- | | `clients.read` | Listar y leer clientes. | `clients:read` | no | | `clients.write` | Crear y actualizar clientes. | `clients:write` | no | | `clients.delete` | Eliminar clientes. | `clients:delete` | ⚠ sí | | `suppliers.read` | Listar y leer proveedores. | `suppliers:read` | no | | `suppliers.write` | Crear y actualizar proveedores. | `suppliers:write` | no | | `suppliers.delete` | Eliminar proveedores. | `suppliers:delete` | ⚠ sí | #### Catálogo — productos, series, impuestos [#catálogo--productos-series-impuestos] | Scope | Concede | Maps to | Sensitive | | ----------------- | ---------------------------------------- | ----------------- | --------- | | `products.read` | Listar y leer el catálogo de productos. | `products:read` | no | | `products.write` | Crear y actualizar productos. | `products:write` | no | | `products.delete` | Eliminar productos. | `products:delete` | ⚠ sí | | `series.read` | Leer series de numeración. | `series:read` | no | | `series.write` | Crear y actualizar series de numeración. | `series:write` | no | | `taxes.read` | Leer tipos impositivos y retenciones. | `taxes:read` | no | | `taxes.write` | Crear y actualizar tipos impositivos. | `taxes:write` | no | #### Ventas — facturas, presupuestos, proformas, albaranes [#ventas--facturas-presupuestos-proformas-albaranes] | Scope | Concede | Maps to | Sensitive | | ---------------------------- | ------------------------------------------------------ | --------------------------- | --------- | | `invoices.read` | Listar y leer facturas. | `invoices:read` | no | | `invoices.write` | Crear y actualizar facturas. | `invoices:write` | no | | `invoices.send` | Enviar facturas por email. | `invoices:send` | no | | `invoices.delete` | Eliminar facturas en borrador. | `invoices:delete` | ⚠ sí | | `invoices.annul` | Anular facturas emitidas. | `invoices:void` | ⚠ sí | | `invoices.create_corrective` | Emitir facturas rectificativas. | `invoices:write` | no | | `quotes.read` | Listar y leer presupuestos. | `quotes:read` | no | | `quotes.write` | Crear y actualizar presupuestos. | `quotes:write` | no | | `quotes.send` | Enviar presupuestos por email. | `quotes:send` | no | | `quotes.delete` | Eliminar presupuestos. | `quotes:delete` | ⚠ sí | | `quotes.convert_to_invoice` | Aceptar/rechazar y convertir presupuestos en facturas. | `quotes:transition` | no | | `proformas.read` | Listar y leer facturas proforma. | `proformas:read` | no | | `proformas.write` | Crear y actualizar proformas. | `proformas:write` | no | | `proformas.send` | Enviar proformas por email. | `proformas:send` | no | | `proformas.delete` | Eliminar proformas. | `proformas:delete` | ⚠ sí | | `proformas.convert` | Convertir proformas en facturas. | `proformas:transition` | no | | `delivery_notes.read` | Listar y leer albaranes. | `delivery_notes:read` | no | | `delivery_notes.write` | Crear, actualizar y enviar albaranes. | `delivery_notes:write` | no | | `delivery_notes.send` | Enviar albaranes por email. | `delivery_notes:write` | no | | `delivery_notes.delete` | Eliminar albaranes. | `delivery_notes:delete` | ⚠ sí | | `delivery_notes.convert` | Convertir albaranes. | `delivery_notes:transition` | no | | `delivery_notes.sign` | Marcar como entregados / firmar albaranes. | `delivery_notes:transition` | ⚠ sí | #### Compras [#compras] | Scope | Concede | Maps to | Sensitive | | ----------------------------- | --------------------------------------- | ------------------------------ | --------- | | `purchase_invoices.read` | Listar y leer facturas de compra. | `purchase_invoices:read` | no | | `purchase_invoices.write` | Crear y actualizar facturas de compra. | `purchase_invoices:write` | no | | `purchase_invoices.mark_paid` | Marcar facturas de compra como pagadas. | `purchase_invoices:transition` | ⚠ sí | | `purchase_invoices.delete` | Eliminar facturas de compra. | `purchase_invoices:delete` | ⚠ sí | <Callout type="info"> **Los scopes de pago son asimétricos entre ventas y compras.** Registrar un pago en una factura de **venta** (`register_invoice_payment`) requiere `invoices:write` — edita la factura. En cambio, registrar un pago en una factura de **compra** (`register_purchase_invoice_payment`) requiere `purchase_invoices:transition`, porque en el lado de compra un pago hace avanzar la factura por su ciclo de vida (pendiente → pagada) en vez de editarla. </Callout> #### Facturas recurrentes [#facturas-recurrentes] | Scope | Concede | Maps to | Sensitive | | ------------------------ | ------------------------------------------ | ------------------------------- | --------- | | `recurring.read` | Listar y leer plantillas recurrentes. | `recurring_invoices:read` | no | | `recurring.write` | Crear y actualizar plantillas recurrentes. | `recurring_invoices:write` | no | | `recurring.pause` | Pausar plantillas recurrentes. | `recurring_invoices:transition` | no | | `recurring.resume` | Reanudar plantillas recurrentes. | `recurring_invoices:transition` | no | | `recurring.generate_now` | Emitir una factura recurrente manualmente. | `recurring_invoices:transition` | ⚠ sí | | `recurring.delete` | Eliminar plantillas recurrentes. | `recurring_invoices:delete` | ⚠ sí | #### Cumplimiento (VeriFactu) [#cumplimiento-verifactu] | Scope | Concede | Maps to | Sensitive | | ---------------- | ------------------------------------------------------------------- | ---------------- | --------- | | `verifactu.read` | Leer registros, eventos, certificados y configuración de VeriFactu. | `verifactu:read` | no | #### Webhooks [#webhooks] | Scope | Concede | Maps to | Sensitive | | ----------------- | ----------------------------------------------------------- | ----------------- | --------- | | `webhooks.read` | Listar webhook endpoints y entregas. | `webhooks:read` | no | | `webhooks.write` | Crear, actualizar, rotar y hacer ping de webhook endpoints. | `webhooks:write` | ⚠ sí | | `webhooks.delete` | Eliminar webhook endpoints. | `webhooks:delete` | ⚠ sí | #### Personal — control horario [#personal--control-horario] Datos de empleados, fichajes, ausencias, horarios de trabajo, presencia, festivos y exportaciones de nómina. Todos los scopes de personal son **sensibles** (PII de empleado y datos de cumplimiento) y requieren el módulo de plan `control_horario` — consulta [Gating por plan y módulo](#plan--module-gating). Las lecturas, `employees.write` y la generación de exportaciones de nómina se conceden en la pantalla de consentimiento; las acciones privilegiadas de escritura y transición no tienen scope OAuth con punto y son solo API key (listadas más abajo en los scopes detallados). | Scope | Concede | Maps to | Sensitive | | ----------------------- | ------------------------------------------------- | ----------------------- | --------- | | `employees.read` | Listar y leer empleados. | `employees:read` | ⚠ sí | | `employees.write` | Crear y actualizar empleados. | `employees:write` | ⚠ sí | | `time_entries.read` | Leer fichajes, saldos y hojas de horas mensuales. | `time_entries:read` | ⚠ sí | | `absences.read` | Listar y leer ausencias, políticas y solicitudes. | `absences:read` | ⚠ sí | | `work_schedules.read` | Leer horarios de trabajo y sus asignaciones. | `work_schedules:read` | ⚠ sí | | `presence.read` | Leer la presencia en vivo y diaria. | `presence:read` | ⚠ sí | | `holidays.read` | Leer el calendario de festivos de la empresa. | `holidays:read` | ⚠ sí | | `payroll_exports.read` | Leer las exportaciones de nómina generadas. | `payroll_exports:read` | ⚠ sí | | `payroll_exports.write` | Generar exportaciones de nómina. | `payroll_exports:write` | ⚠ sí | ### Macros [#macros] Paquetes de conveniencia que se expanden a una lista de scopes simples en el momento de emitir el token. El token persiste los scopes **expandidos** — las macros nunca se almacenan. | Macro | Concede | Sensitive | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------- | | `factuarea.read` | Acceso de lectura completo a todo (sin escrituras). | no | | `factuarea.write` | Leer todo, además de crear/actualizar documentos y enviar emails. | no | | `factuarea.full` | Leer, escribir, enviar y acciones destructivas (eliminar, anular, marcar como pagada, firmar). Excluye las escrituras de VeriFactu. | ⚠ sí | ## El super-scope `*` [#el-super-scope-] Una credencial que tiene `*` cubre **todos** los scopes — las <Stat n="tools" /> tools en el caso de una API key. Es el equivalente a una clave de propietario. Resérvalo para migraciones puntuales o automatizaciones de propietario totalmente confiables; para todo lo demás, prefiere el conjunto de scopes más reducido. El super-scope está disponible para las API keys; el consentimiento OAuth concede scopes explícitos (o macros), nunca un `*` directo. ## Cómo los scopes OAuth se convierten en scopes detallados [#cómo-los-scopes-oauth-se-convierten-en-scopes-detallados] Cuando se emite un token OAuth, sus scopes con punto se traducen una vez al catálogo detallado que aplican las tools. Conviene conocer algunas reconciliaciones: * `recurring.*` se mapea al recurso `recurring_invoices:*`. * `invoices.create_corrective` se mapea a `invoices:write` (crear es una escritura). * `invoices.annul` se mapea a `invoices:void`. * Las acciones de ciclo de vida (`*.convert`, `*.sign`, `*.pause`, `*.resume`, `*.generate_now`, `*.mark_paid`, `quotes.convert_to_invoice`) se mapean al scope `:transition` del recurso. * Cualquier scope de lectura sobre un documento también concede las utilidades de lectura transversales `pdfs:read` (descargar su PDF/recibo) y `events:read` (su registro de actividad). * `verifactu.write` y `delivery_notes:gdpr_forget` **no** tienen scope OAuth con punto — son inalcanzables vía OAuth por diseño. * `facturae:read` / `facturae:write` aún **no están en el catálogo de consentimiento OAuth** — las tools de FacturaE (FACe) solo son accesibles con API key por ahora. ## Scopes detallados sin scope OAuth (solo API key) [#scopes-detallados-sin-scope-oauth-solo-api-key] Algunos scopes detallados viven en el catálogo cerrado `recurso:accion` que usan las API keys, pero **no tienen equivalente OAuth con punto** — nunca se conceden a través de una pantalla de consentimiento de terceros y solo son accesibles con API key. Concédelos directamente en la key (o vía el super-scope `*`). Algunos están gateados por un módulo de integración —entonces la empresa de la key debe tener el plan correspondiente (consulta [Gating por plan y módulo](#plan--module-gating))—; el resto son scopes de cuenta propia y de gestoría. | Scope | Concede | Gating de módulo | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `stripe_autoinvoicing:read` | Leer el estado de la integración Stripe Connect, la configuración de auto-facturación y las cuentas conectadas, y listar cobros/rectificativas auto-facturados. | `integration_stripe` | | `stripe_autoinvoicing:write` | Activar/desactivar la auto-facturación de cobros Stripe, fijar la serie auto-emitida y editar/desconectar cuentas conectadas. | `integration_stripe` | | `payouts:read` | Leer los payouts de Stripe ingeridos y su estado de conciliación bancaria. | `integration_stripe` | Estos scopes habilitan las [tools de Pagos y pasarelas](/mcp/tools#payments). Un segundo grupo de scopes solo para API key gobierna la gestión de **cuenta propia** y de **gestoría** — tus propias credenciales y, para asesorías, las empresas hijas que gestionas y sus API keys. `companies:*` requiere el módulo del plan de gestoría; el resto no tienen gating de módulo. | Scope | Concede | Gating de módulo | | ------------------ | ---------------------------------------------------------------------------------------------------- | ---------------- | | `account:write` | Gestionar tus propias API keys (crear, rotar, revocar) y actualizar la personalización de la cuenta. | — | | `companies:read` | Listar y leer las empresas gestionadas (sub-cuentas hijas). | `gestoria` | | `companies:write` | Crear, actualizar, activar y desactivar empresas gestionadas. | `gestoria` | | `companies:delete` | Archivar empresas gestionadas. | `gestoria` | | `api_keys:read` | Listar y leer las API keys de las empresas gestionadas. | — | | `api_keys:write` | Crear, rotar y revocar las API keys de las empresas gestionadas. | — | | `api_keys:delete` | Eliminar permanentemente las API keys de las empresas gestionadas. | — | Un tercer grupo cubre las acciones de escritura y transición de **personal** (control horario). Sus lecturas se conceden por OAuth (consulta los scopes de consentimiento de Personal más arriba), pero estos scopes privilegiados no tienen equivalente OAuth con punto — son solo API key, el espejo de `verifactu:write`. Todos requieren el módulo de plan `control_horario`. | Scope | Concede | Gating de módulo | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------- | | `employees:delete` | Eliminar empleados de forma permanente. | `control_horario` | | `time_entries:write` | Fichar entrada/salida, registrar entradas manuales, gestionar correcciones de fichaje y el cierre mensual del registro. | `control_horario` | | `absences:write` | Crear y gestionar tipos, políticas y solicitudes de ausencia. | `control_horario` | | `absences:transition` | Aprobar, rechazar y cancelar solicitudes de ausencia. | `control_horario` | | `work_schedules:write` | Crear, actualizar, asignar y archivar horarios de trabajo. | `control_horario` | <Callout> `verifactu:write`, `facturae:read`, `facturae:write` y `delivery_notes:gdpr_forget` también son scopes detallados solo para API key (descritos arriba) — se aplican en la frontera de la tool como cualquier otro scope, pero no tienen contraparte en el consentimiento OAuth. </Callout> ## Gating por plan y módulo [#gating-por-plan-y-módulo] La mayoría de las tools publicadas solo aplican una comprobación de **scope**: son accesibles en cuanto la credencial tiene el scope requerido. Dos familias además están **limitadas por módulo**. Las [tools de Pagos y pasarelas](/mcp/tools#payments) mapean sus scopes (`stripe_autoinvoicing:*`, `payouts:read`) al módulo `integration_stripe`. Las tools de personal (Empleados, Asientos de empleado, Horarios de trabajo, Control horario, Ausencias, Presencia, Festivos) mapean sus scopes (`employees:*`, `time_entries:*`, `absences:*`, `work_schedules:*`, `presence:read`, `holidays:read`, `payroll_exports:*`) al módulo `control_horario`. Cuando el plan de la empresa no incluye el módulo, el servidor oculta esas tools de `tools/list` y devuelve `module_not_in_plan` (`-32005`) ante una llamada directa. * Los **límites de uso** del plan (p. ej. cuotas mensuales de documentos) se aplican en el momento de la llamada y se manifiestan como `plan_limit_exceeded` (`-32004`). Consulta [Errores y límites de peticiones](/mcp/errors). Toda la superficie pública MCP también requiere que la empresa tenga un **plan de Factuarea activo** — el acceso a la API está incluido en todos los planes; de lo contrario, cada llamada devuelve `addon_not_active` (`-32007`). --- # Catálogo de tools (/es/mcp/tools) El servidor MCP de Factuarea publica **<Stat n="tools" /> tools** en 27 dominios. Esta página es la lista canónica; el esquema de entrada de cada tool se descubre en tiempo de ejecución mediante `tools/list`. Cada tool se corresponde con la misma operación en la Referencia de la REST API. ## Cómo leer este catálogo [#cómo-leer-este-catálogo] * **Tool** — el nombre de la tool MCP que invoca tu agente. * **Scope** — el scope granular que debe tener la credencial para que la tool aparezca en `tools/list` y sea invocable. Consulta [Scopes y permisos](/mcp/scopes). * **Category** — el bucket de límite de peticiones al que pertenece la tool: `read`, `write`, `destructive`, `send` o `generate`. Consulta [Errores y límites de peticiones](/mcp/errors). Una credencial solo ve las tools que cubren sus scopes. Una API key con el super-scope `*` las ve las <Stat n="tools" />; una key más restringida o un token OAuth ve un subconjunto. <Callout type="warn"> **¹ Restringidas a OAuth (solo API key).** <Stat n="oauth_restricted" /> tools son accesibles **únicamente** con una API key, nunca a través de una app OAuth de terceros: las 8 tools `verifactu:write`, `forget_delivery_note_signature` (`delivery_notes:gdpr_forget`), las 5 tools de FacturaE (los scopes `facturae:read` / `facturae:write` aún no están en el catálogo de consentimiento OAuth), las 13 tools de Pagos y pasarelas (los scopes `stripe_autoinvoicing:*`, `payouts:read` e `integration_events:*` son scopes granulares, solo API key), las 3 tools de Correos enviados y las 2 de registro de peticiones (`emails:read` y `developers:read` inspeccionan el tráfico de tu propia integración: son scopes de primera parte, nunca se conceden por consentimiento de terceros), las 17 tools de Gestoría (`companies:*` y `api_keys:*`: gestionar sub-cuentas y credenciales nunca se concede a terceros por consentimiento OAuth — incluye `get_consolidated_workforce`), las 4 tools de escritura de cuenta (`account:write`: crear, rotar o revocar API keys y actualizar la personalización de la cuenta) y las 33 tools de control horario cuyos scopes de escritura/transición (`time_entries:write`, `absences:write`, `absences:transition`, `work_schedules:write`) son granulares y solo API key, fuera del catálogo de consentimiento OAuth (sus lecturas y `employees:write` sí se conceden por OAuth). Por tanto, las apps OAuth alcanzan **<Stat n="oauth_reachable" />** de las <Stat n="tools" /> tools. Consulta la [política de canales](/mcp/connect#channel-policy). </Callout> ## Dominios [#dominios] ### Facturas [#invoice] Facturas de venta: búsqueda, CRUD, rectificativas, ledger de pagos parciales, transiciones (paid/sent/void/annul), programación, operaciones masivas, recordatorios, enlaces públicos, PDFs y exportación a Excel. *(43 tools)* | Herramienta | Scope | Categoría | | -------------------------------------- | ----------------- | ----------- | | `can_annul_invoice` | `invoices:read` | read | | `check_invoice_simplified_eligibility` | `invoices:read` | read | | `export_invoices_excel` | `invoices:read` | read | | `find_invoice_by_external_id` | `invoices:read` | read | | `find_invoice_by_number` | `invoices:read` | read | | `get_available_quarters` | `invoices:read` | read | | `get_invoice` | `invoices:read` | read | | `get_invoice_activities` | `invoices:read` | read | | `get_invoice_correctives` | `invoices:read` | read | | `get_invoice_payment_receipt` | `pdfs:read` | read | | `get_invoice_pdf` | `pdfs:read` | read | | `get_invoice_public_link` | `invoices:read` | read | | `get_invoice_stats` | `invoices:read` | read | | `get_invoice_statuses` | `invoices:read` | read | | `list_invoice_payments` | `invoices:read` | read | | `list_payment_methods` | `invoices:read` | read | | `preview_invoice_reminder` | `invoices:read` | read | | `search_invoices` | `invoices:read` | read | | `annul_invoice` | `invoices:void` | write | | `assign_invoice_real_number` | `invoices:write` | write | | `bulk_change_invoice_status` | `invoices:write` | write | | `bulk_create_invoices` | `invoices:write` | write | | `bulk_delete_invoices` | `invoices:delete` | destructive | | `bulk_invoices_pdf_link` | `pdfs:read` | generate | | `bulk_send_invoices` | `invoices:send` | send | | `create_corrective_invoice` | `invoices:write` | write | | `create_invoice` | `invoices:write` | write | | `delete_invoice` | `invoices:delete` | destructive | | `duplicate_invoice` | `invoices:write` | write | | `mark_invoice_as_paid` | `invoices:write` | write | | `mark_invoice_as_sent` | `invoices:write` | write | | `mark_invoice_unsent` | `invoices:write` | write | | `quarterly_send_email` | `invoices:send` | send | | `register_invoice_payment` | `invoices:write` | write | | `reschedule_invoice` | `invoices:write` | write | | `schedule_invoice` | `invoices:write` | write | | `send_invoice` | `invoices:send` | send | | `send_invoice_reminder` | `invoices:send` | send | | `substitute_simplified_invoice` | `invoices:write` | write | | `unschedule_invoice` | `invoices:write` | write | | `update_invoice` | `invoices:write` | write | | `update_invoice_public_link` | `invoices:write` | write | | `void_invoice` | `invoices:void` | write | ### Clientes [#client] CRM de clientes: búsqueda, CRUD, creación/borrado masivo, búsqueda por NIF o ID externo, importación CSV, verificación censal AEAT, estadísticas y actividad. *(13 tools)* | Herramienta | Scope | Categoría | | ---------------------------- | ---------------- | ----------- | | `find_client_by_external_id` | `clients:read` | read | | `find_client_by_tax_id` | `clients:read` | read | | `get_client` | `clients:read` | read | | `get_client_activities` | `clients:read` | read | | `get_client_stats` | `clients:read` | read | | `search_clients` | `clients:read` | read | | `verify_client_census` | `clients:read` | read | | `bulk_create_clients` | `clients:write` | write | | `bulk_delete_clients` | `clients:delete` | destructive | | `create_client` | `clients:write` | write | | `delete_client` | `clients:delete` | destructive | | `import_clients_csv` | `clients:write` | write | | `update_client` | `clients:write` | write | ### Proveedores [#supplier] CRM de proveedores: búsqueda, CRUD, borrado/estado masivo, búsqueda por NIF o ID externo, activar/desactivar, estadísticas y actividad. *(12 tools)* | Herramienta | Scope | Categoría | | ------------------------------ | ------------------ | ----------- | | `find_supplier_by_external_id` | `suppliers:read` | read | | `find_supplier_by_tax_id` | `suppliers:read` | read | | `get_supplier` | `suppliers:read` | read | | `get_supplier_activities` | `suppliers:read` | read | | `get_supplier_stats` | `suppliers:read` | read | | `search_suppliers` | `suppliers:read` | read | | `bulk_change_supplier_status` | `suppliers:write` | write | | `bulk_delete_suppliers` | `suppliers:delete` | destructive | | `create_supplier` | `suppliers:write` | write | | `delete_supplier` | `suppliers:delete` | destructive | | `toggle_supplier_active` | `suppliers:write` | write | | `update_supplier` | `suppliers:write` | write | ### Productos [#product] Catálogo: búsqueda, CRUD, stock/borrado/estado masivo, búsqueda por SKU o ID externo, analítica de ventas, informes de stock bajo y media (galería/vídeo). *(21 tools)* | Herramienta | Scope | Categoría | | -------------------------------- | ----------------- | ----------- | | `download_product_gallery_image` | `products:read` | read | | `download_product_video` | `products:read` | read | | `find_product_by_external_id` | `products:read` | read | | `find_product_by_sku` | `products:read` | read | | `get_product` | `products:read` | read | | `get_product_activities` | `products:read` | read | | `get_product_sales_analytics` | `products:read` | read | | `get_product_stats` | `products:read` | read | | `low_stock_report` | `products:read` | read | | `search_products` | `products:read` | read | | `bulk_change_product_status` | `products:write` | write | | `bulk_delete_products` | `products:delete` | destructive | | `bulk_update_stock` | `products:write` | write | | `create_product` | `products:write` | write | | `delete_product` | `products:delete` | destructive | | `delete_product_gallery_image` | `products:delete` | destructive | | `delete_product_video` | `products:delete` | destructive | | `toggle_product_active` | `products:write` | write | | `update_product` | `products:write` | write | | `upload_product_gallery_image` | `products:write` | write | | `upload_product_video` | `products:write` | write | ### Presupuestos [#quote] Presupuestos: búsqueda, CRUD, aceptar/rechazar, enviar, convertir a factura, operaciones masivas, búsqueda por ID externo, enlaces públicos y PDFs. *(20 tools)* | Herramienta | Scope | Categoría | | --------------------------- | ------------------- | ----------- | | `find_quote_by_external_id` | `quotes:read` | read | | `get_quote` | `quotes:read` | read | | `get_quote_pdf` | `pdfs:read` | read | | `get_quote_public_link` | `quotes:read` | read | | `get_quote_stats` | `quotes:read` | read | | `get_quote_statuses` | `quotes:read` | read | | `search_quotes` | `quotes:read` | read | | `accept_quote` | `quotes:transition` | write | | `bulk_change_quote_status` | `quotes:transition` | write | | `bulk_delete_quotes` | `quotes:delete` | destructive | | `bulk_quotes_pdf_link` | `pdfs:read` | generate | | `bulk_send_quotes` | `quotes:send` | send | | `convert_quote` | `quotes:transition` | write | | `create_quote` | `quotes:write` | write | | `delete_quote` | `quotes:delete` | destructive | | `duplicate_quote` | `quotes:write` | write | | `reject_quote` | `quotes:transition` | write | | `send_quote` | `quotes:send` | send | | `update_quote` | `quotes:write` | write | | `update_quote_public_link` | `quotes:write` | write | ### Facturas proforma [#proforma] Proformas: búsqueda, CRUD, aceptar/rechazar, enviar, convertir, operaciones masivas, búsqueda por ID externo, enlaces públicos y PDFs. *(20 tools)* | Herramienta | Scope | Categoría | | ------------------------------ | ---------------------- | ----------- | | `find_proforma_by_external_id` | `proformas:read` | read | | `get_proforma` | `proformas:read` | read | | `get_proforma_pdf` | `pdfs:read` | read | | `get_proforma_public_link` | `proformas:read` | read | | `get_proforma_stats` | `proformas:read` | read | | `get_proforma_statuses` | `proformas:read` | read | | `search_proformas` | `proformas:read` | read | | `accept_proforma` | `proformas:transition` | write | | `bulk_change_proforma_status` | `proformas:transition` | write | | `bulk_delete_proformas` | `proformas:delete` | destructive | | `bulk_proformas_pdf_link` | `pdfs:read` | generate | | `bulk_send_proformas` | `proformas:send` | send | | `convert_proforma` | `proformas:transition` | write | | `create_proforma` | `proformas:write` | write | | `delete_proforma` | `proformas:delete` | destructive | | `duplicate_proforma` | `proformas:write` | write | | `reject_proforma` | `proformas:transition` | write | | `send_proforma` | `proformas:send` | send | | `update_proforma` | `proformas:write` | write | | `update_proforma_public_link` | `proformas:write` | write | ### Albaranes [#delivery-note] Albaranes: búsqueda, CRUD, entregar/cancelar/firmar, convertir, enviar, operaciones masivas, búsqueda por ID externo, enlaces públicos, PDFs y borrado GDPR de la firma. *(22 tools)* | Herramienta | Scope | Categoría | | ----------------------------------- | ------------------------------ | ----------- | | `find_delivery_note_by_external_id` | `delivery_notes:read` | read | | `get_delivery_note` | `delivery_notes:read` | read | | `get_delivery_note_pdf` | `pdfs:read` | read | | `get_delivery_note_public_link` | `delivery_notes:read` | read | | `get_delivery_note_stats` | `delivery_notes:read` | read | | `get_delivery_note_statuses` | `delivery_notes:read` | read | | `search_delivery_notes` | `delivery_notes:read` | read | | `bulk_change_delivery_note_status` | `delivery_notes:transition` | write | | `bulk_delete_delivery_notes` | `delivery_notes:delete` | destructive | | `bulk_delivery_notes_pdf_link` | `pdfs:read` | generate | | `bulk_send_delivery_notes` | `delivery_notes:write` | send | | `cancel_delivery_note` | `delivery_notes:transition` | write | | `convert_delivery_note` | `delivery_notes:transition` | write | | `create_delivery_note` | `delivery_notes:write` | write | | `delete_delivery_note` | `delivery_notes:delete` | destructive | | `duplicate_delivery_note` | `delivery_notes:write` | write | | `forget_delivery_note_signature` | `delivery_notes:gdpr_forget` ¹ | destructive | | `mark_delivery_note_delivered` | `delivery_notes:transition` | write | | `send_delivery_note` | `delivery_notes:write` | send | | `sign_delivery_note` | `delivery_notes:transition` | write | | `update_delivery_note` | `delivery_notes:write` | write | | `update_delivery_note_public_link` | `delivery_notes:write` | write | ### Facturas de compra [#purchase-invoice] Facturas de compra: búsqueda, CRUD, marcar como pagada, ledger de pagos parciales, operaciones masivas, listas de pendientes/vencidas, búsqueda por ID externo, adjuntos de archivo y recibos de pago. *(18 tools)* | Herramienta | Scope | Categoría | | -------------------------------------- | ------------------------------ | ----------- | | `download_purchase_invoice_file` | `purchase_invoices:read` | read | | `find_purchase_invoice_by_external_id` | `purchase_invoices:read` | read | | `get_purchase_invoice` | `purchase_invoices:read` | read | | `get_purchase_invoice_payment_receipt` | `pdfs:read` | read | | `get_purchase_invoice_stats` | `purchase_invoices:read` | read | | `list_overdue_purchase_invoices` | `purchase_invoices:read` | read | | `list_pending_purchase_invoices` | `purchase_invoices:read` | read | | `list_purchase_invoice_payments` | `purchase_invoices:read` | read | | `search_purchase_invoices` | `purchase_invoices:read` | read | | `attach_purchase_invoice_file` | `purchase_invoices:write` | write | | `bulk_change_purchase_invoice_status` | `purchase_invoices:transition` | write | | `bulk_delete_purchase_invoices` | `purchase_invoices:delete` | destructive | | `create_purchase_invoice` | `purchase_invoices:write` | write | | `delete_purchase_invoice` | `purchase_invoices:delete` | destructive | | `delete_purchase_invoice_file` | `purchase_invoices:write` | write | | `mark_purchase_invoice_paid` | `purchase_invoices:transition` | write | | `register_purchase_invoice_payment` | `purchase_invoices:transition` | write | | `update_purchase_invoice` | `purchase_invoices:write` | write | ### Facturas recurrentes [#recurring-invoice] Plantillas recurrentes: búsqueda, CRUD, crear desde una factura, activar/pausar/reanudar/cancelar/saltar, previsualización, búsqueda por ID externo, logs y actividad. *(17 tools)* | Herramienta | Scope | Categoría | | --------------------------------------- | ------------------------------- | ----------- | | `find_recurring_invoice_by_external_id` | `recurring_invoices:read` | read | | `get_recurring_invoice` | `recurring_invoices:read` | read | | `get_recurring_invoice_stats` | `recurring_invoices:read` | read | | `list_recurring_invoice_activities` | `recurring_invoices:read` | read | | `list_recurring_invoice_logs` | `recurring_invoices:read` | read | | `preview_recurring_invoice` | `recurring_invoices:read` | read | | `search_recurring_invoices` | `recurring_invoices:read` | read | | `activate_recurring_invoice` | `recurring_invoices:transition` | write | | `bulk_delete_recurring_invoices` | `recurring_invoices:delete` | destructive | | `cancel_recurring_invoice` | `recurring_invoices:transition` | write | | `create_recurring_invoice` | `recurring_invoices:write` | write | | `create_recurring_invoice_from_invoice` | `recurring_invoices:write` | write | | `delete_recurring_invoice` | `recurring_invoices:delete` | destructive | | `pause_recurring_invoice` | `recurring_invoices:transition` | write | | `resume_recurring_invoice` | `recurring_invoices:transition` | write | | `skip_recurring_invoice` | `recurring_invoices:write` | write | | `update_recurring_invoice` | `recurring_invoices:write` | write | ### Series [#series] Series de numeración (inmutables por continuidad fiscal): búsqueda, crear, archivar/desarchivar, marcar por defecto, arrancar las cuatro series por defecto de una empresa recién dada de alta, estadísticas y actividad. *(12 tools)* | Herramienta | Scope | Categoría | | ----------------------------- | -------------- | --------- | | `find_series_by_code` | `series:read` | read | | `get_default_series_for_type` | `series:read` | read | | `get_series` | `series:read` | read | | `get_series_activities` | `series:read` | read | | `get_series_stats` | `series:read` | read | | `list_active_series` | `series:read` | read | | `search_series` | `series:read` | read | | `archive_series` | `series:write` | write | | `bootstrap_series` | `series:write` | write | | `create_series` | `series:write` | write | | `mark_series_as_default` | `series:write` | write | | `unarchive_series` | `series:write` | write | ### Impuestos [#tax] Tipos impositivos (catálogo global): búsqueda, CRUD-lite, valores por defecto, comprobaciones de uso, cálculos de impuesto/totales y el catálogo fiscal AEAT de solo lectura (regímenes, causas de exención, tipos de IRPF y los pares legales IVA ↔ recargo de equivalencia). *(16 tools)* | Herramienta | Scope | Categoría | | ------------------------------- | ------------- | --------- | | `calculate_tax` | `taxes:read` | read | | `calculate_totals` | `taxes:read` | read | | `check_tax_in_use` | `taxes:read` | read | | `get_active_taxes` | `taxes:read` | read | | `get_tax` | `taxes:read` | read | | `get_tax_catalog` | `taxes:read` | read | | `get_tax_defaults_for_document` | `taxes:read` | read | | `get_tax_stats` | `taxes:read` | read | | `get_taxes_by_type` | `taxes:read` | read | | `get_taxes_for_purchases` | `taxes:read` | read | | `get_taxes_for_sales` | `taxes:read` | read | | `search_taxes` | `taxes:read` | read | | `create_tax` | `taxes:write` | write | | `set_tax_as_default` | `taxes:write` | write | | `set_tax_default_for_document` | `taxes:write` | write | | `toggle_tax_active` | `taxes:write` | write | ### VeriFactu [#verifactu] SIF de la AEAT: registros, eventos, validación de cadena, reintento, subsanación de registros rechazados, certificados, ajustes, declaración responsable y log de acceso a la AEAT. *(27 tools)* | Herramienta | Scope | Categoría | | ----------------------------------------- | ------------------- | --------- | | `find_verifactu_record_by_csv` | `verifactu:read` | read | | `find_verifactu_record_by_huella` | `verifactu:read` | read | | `find_verifactu_record_by_invoice_number` | `verifactu:read` | read | | `get_active_company_certificate` | `verifactu:read` | read | | `get_aeat_access_record` | `verifactu:read` | read | | `get_declaracion_responsable` | `verifactu:read` | read | | `get_declaracion_responsable_history` | `verifactu:read` | read | | `get_invoice_verifactu` | `verifactu:read` | read | | `get_verifactu_activities` | `verifactu:read` | read | | `get_verifactu_config` | `verifactu:read` | read | | `get_verifactu_event` | `verifactu:read` | read | | `get_verifactu_event_summary` | `verifactu:read` | read | | `get_verifactu_record` | `verifactu:read` | read | | `get_verifactu_stats` | `verifactu:read` | read | | `list_aeat_access_records` | `verifactu:read` | read | | `list_company_certificates` | `verifactu:read` | read | | `list_verifactu_events` | `verifactu:read` | read | | `search_verifactu_records` | `verifactu:read` | read | | `validate_verifactu_chain` | `verifactu:read` | read | | `activate_company_certificate` | `verifactu:write` ¹ | write | | `create_invoice_verifactu` | `verifactu:write` ¹ | write | | `retry_verifactu_event` | `verifactu:write` ¹ | write | | `retry_verifactu_record` | `verifactu:write` ¹ | write | | `revoke_company_certificate` | `verifactu:write` ¹ | write | | `subsanar_verifactu_record` | `verifactu:write` ¹ | write | | `update_verifactu_settings` | `verifactu:write` ¹ | write | | `upload_company_certificate` | `verifactu:write` ¹ | write | ### FacturaE (FACe) [#facturae] Facturación electrónica B2G: descarga el XML FacturaE 3.2.2 de una factura y gestiona sus presentaciones a FACe (enviar, seguir, cancelar). *(5 tools)* | Herramienta | Scope | Categoría | | ------------------------------- | ------------------ | --------- | | `get_face_submission` | `facturae:read` ¹ | read | | `list_invoice_face_submissions` | `facturae:read` ¹ | read | | `cancel_face_submission` | `facturae:write` ¹ | write | | `get_invoice_facturae_link` | `facturae:read` ¹ | generate | | `send_invoice_to_face` | `facturae:write` ¹ | write | ### Webhooks y eventos [#webhook] Endpoints de webhook, entregas, rotación de secreto, ping/replay, eventos de prueba y el catálogo de eventos publicados. *(13 tools)* | Herramienta | Scope | Categoría | | -------------------------- | ----------------- | --------- | | `get_event` | `events:read` | read | | `get_webhook_delivery` | `webhooks:read` | read | | `get_webhook_endpoint` | `webhooks:read` | read | | `list_events` | `events:read` | read | | `list_webhook_deliveries` | `webhooks:read` | read | | `search_webhook_endpoints` | `webhooks:read` | read | | `create_webhook_endpoint` | `webhooks:write` | write | | `delete_webhook_endpoint` | `webhooks:delete` | write | | `ping_webhook_endpoint` | `webhooks:write` | write | | `replay_webhook_delivery` | `webhooks:write` | write | | `rotate_webhook_secret` | `webhooks:write` | write | | `test_webhook_endpoint` | `webhooks:write` | write | | `update_webhook_endpoint` | `webhooks:write` | write | ### Cuenta [#account] Identidad fiscal y personalización de la cuenta: verifica el par nombre + NIF registrado de la empresa contra el censo de la AEAT, consulta plantillas de personalización y actualiza la personalización de la cuenta. *(<Stat n="domain:Account" /> tools)* | Herramienta | Scope | Categoría | | --------------------------------------- | ----------------- | --------- | | `get_account_billing` | `account:read` | read | | `get_account_personalization_templates` | `account:read` | read | | `update_account_personalization` | `account:write` ¹ | write | | `verify_account_census` | `account:read` | write | ### API keys [#api-key] API keys self-service de tu propio tenant: listar, crear, consultar, rotar el secreto y revocar. El secreto se devuelve una sola vez, al crear y al rotar. *(5 tools)* | Herramienta | Scope | Categoría | | ----------------------- | ----------------- | --------- | | `get_api_key` | `account:read` | read | | `list_api_keys` | `account:read` | read | | `create_api_key` | `account:write` ¹ | write | | `revoke_api_key` | `account:write` ¹ | write | | `rotate_api_key_secret` | `account:write` ¹ | write | ### Gestoría [#gestoria] Modo gestoría: gestiona las empresas hijas del tenant maestro y sus child API keys — crear/listar/consultar/actualizar/eliminar, activar/desactivar, previsualización del coste por asiento, estado de aprovisionamiento, API keys por empresa y un resumen consolidado del cumplimiento horario de las empresas hijas. *(17 tools)* | Herramienta | Scope | Categoría | | --------------------------------- | -------------------- | --------- | | `get_company` | `companies:read` ¹ | read | | `get_company_api_key` | `api_keys:read` ¹ | read | | `get_company_creation_status` | `companies:read` ¹ | read | | `get_company_seat_charge_preview` | `companies:read` ¹ | read | | `get_consolidated_workforce` | `companies:read` ¹ | read | | `list_companies` | `companies:read` ¹ | read | | `list_company_api_keys` | `api_keys:read` ¹ | read | | `activate_companies` | `companies:write` ¹ | write | | `activate_company` | `companies:write` ¹ | write | | `create_company` | `companies:write` ¹ | write | | `create_company_api_key` | `api_keys:write` ¹ | write | | `deactivate_company` | `companies:write` ¹ | write | | `delete_company` | `companies:delete` ¹ | write | | `revoke_company_api_key` | `api_keys:write` ¹ | write | | `rotate_company_api_key_secret` | `api_keys:write` ¹ | write | | `update_company` | `companies:write` ¹ | write | | `verify_company_creation` | `companies:write` ¹ | write | ### Pagos y pasarelas [#payments] Auto-facturación de pasarelas de pago y bandeja de eventos de las pasarelas: estado y configuración de Stripe Connect, cuentas conectadas (multi-tienda), cobros y rectificativas auto-facturados, payouts de Stripe, y los eventos que las pasarelas enviaron a Factuarea — qué llegó, qué produjo y, cuando no produjo nada, el motivo tipado. Es el sitio donde mirar cuando un cobro no ha generado factura. Hoy solo Stripe está disponible (GoCardless y MONEI aún no lo están). Estas tools son **solo API key** — sus scopes no están en el catálogo de consentimiento OAuth. Las de auto-facturación y payouts de Stripe están además gated por el módulo `integration_stripe` (plan Empresario en adelante); las de la bandeja de eventos, no. *(13 tools)* <Callout type="warn"> `replay_integrations_event` tiene efecto fiscal real. Si la causa que impidió facturar ya está resuelta, reprocesar un evento parqueado **puede emitir una factura de verdad**, con su número de serie y su alta en VeriFactu. No es un reintento inocuo: confírmalo con la persona usuaria antes de invocarla. No duplica facturas: el reproceso vuelve a pasar por la misma comprobación de idempotencia del intento original. </Callout> | Herramienta | Scope | Categoría | | -------------------------------------- | ------------------------------ | --------- | | `get_integrations_event` | `integration_events:read` ¹ | read | | `get_payout` | `payouts:read` ¹ | read | | `get_stripe_autoinvoicing_config` | `stripe_autoinvoicing:read` ¹ | read | | `get_stripe_connected_account` | `stripe_autoinvoicing:read` ¹ | read | | `list_integrations_events` | `integration_events:read` ¹ | read | | `list_stripe_autoinvoiced_correctives` | `stripe_autoinvoicing:read` ¹ | read | | `list_stripe_autoinvoiced_payments` | `stripe_autoinvoicing:read` ¹ | read | | `list_stripe_connected_accounts` | `stripe_autoinvoicing:read` ¹ | read | | `search_payouts` | `payouts:read` ¹ | read | | `disconnect_stripe_connected_account` | `stripe_autoinvoicing:write` ¹ | write | | `replay_integrations_event` | `integration_events:write` ¹ | write | | `update_stripe_autoinvoicing_config` | `stripe_autoinvoicing:write` ¹ | write | | `update_stripe_connected_account` | `stripe_autoinvoicing:write` ¹ | write | ### Correos enviados [#email] Registro de correos enviados: lista los correos que Factuarea ha enviado en nombre de la empresa (facturas, presupuestos, albaranes, recordatorios de pago), consulta uno por su id y resume de una sola vez cómo acabó el envío de un lote de hasta 100 documentos. Úsalo para responder «¿se envió el correo de esta factura?» y para investigar envíos fallidos. *(3 tools)* <Callout type="warn"> El estado describe la **entrega al servidor SMTP saliente, no la entrega real**. `sent` significa que el servidor de correo saliente aceptó el mensaje: aun así puede rebotar o acabar en spam sin que Factuarea se entere. No existen los estados `delivered`, `bounced` ni `opened`; `queued`, `sending`, `sent` y `failed` son los únicos. Nunca afirmes que la persona destinataria lo recibió, lo abrió o lo leyó. </Callout> | Herramienta | Scope | Categoría | | ----------------------- | --------------- | --------- | | `get_emails` | `emails:read` ¹ | read | | `get_emails_indicators` | `emails:read` ¹ | read | | `list_emails` | `emails:read` ¹ | read | ### Registro de peticiones a la API [#request-log] El tráfico de tu propia integración contra la API pública v1 en los últimos 30 días: lista las llamadas con su método, ruta, código de estado, duración, prefijo de API key y entorno, con filtros por solo errores o por cualquiera de esos campos, y consulta una concreta por su `request_id`, el identificador opaco que la API devuelve en la cabecera de cada respuesta. Úsalo para depurar una integración: qué llamó, cuándo, con qué código de estado y cuánto tardó. Las cabeceras, el cuerpo y la query string **no** se almacenan y nunca se devuelven. *(2 tools)* | Herramienta | Scope | Categoría | | ------------------------------ | ------------------- | --------- | | `get_developers_request_logs` | `developers:read` ¹ | read | | `list_developers_request_logs` | `developers:read` ¹ | read | ### Empleados [#employee] Plantilla de personal: búsqueda, alta/edición/baja/reactivación, estadísticas y el ciclo de invitaciones (enviar/reenviar/cancelar y listar invitaciones de empleado). Requiere el módulo `control_horario`. *(12 tools)* | Herramienta | Scope | Categoría | | ------------------------------ | ----------------- | --------- | | `find_employee_by_external_id` | `employees:read` | read | | `get_employee` | `employees:read` | read | | `get_employee_stats` | `employees:read` | read | | `list_employee_invitations` | `employees:read` | read | | `search_employees` | `employees:read` | read | | `cancel_employee_invitation` | `employees:write` | write | | `create_employee` | `employees:write` | write | | `deactivate_employee` | `employees:write` | write | | `reactivate_employee` | `employees:write` | write | | `resend_employee_invitation` | `employees:write` | write | | `send_employee_invitation` | `employees:write` | write | | `update_employee` | `employees:write` | write | ### Asientos de empleado [#employee-seat] Facturación del add-on de asientos: la suscripción de asientos por contrato — previsualizar y consultar el cargo y la facturación del asiento, suscribir, cambiar la cantidad de asientos y cancelar el add-on. Requiere el módulo `control_horario`. *(5 tools)* | Herramienta | Scope | Categoría | | ------------------------------- | ----------------- | --------- | | `get_employee_seat_billing` | `employees:read` | read | | `preview_employee_seat_charge` | `employees:read` | read | | `cancel_employee_seat_addon` | `employees:write` | write | | `change_employee_seat_quantity` | `employees:write` | write | | `subscribe_employee_seat_addon` | `employees:write` | write | ### Horarios de trabajo [#work-schedule] Horarios de trabajo semanales y sus asignaciones: búsqueda, CRUD, archivar/desarchivar, asignar/desasignar a empleados y consultar el horario efectivo de un empleado. Requiere el módulo `control_horario`. *(11 tools)* | Herramienta | Scope | Categoría | | -------------------------------- | ------------------------ | --------- | | `get_employee_work_schedule` | `work_schedules:read` | read | | `get_work_schedule` | `work_schedules:read` | read | | `get_work_schedule_stats` | `work_schedules:read` | read | | `list_work_schedule_assignments` | `work_schedules:read` | read | | `search_work_schedules` | `work_schedules:read` | read | | `archive_work_schedule` | `work_schedules:write` ¹ | write | | `assign_work_schedule` | `work_schedules:write` ¹ | write | | `create_work_schedule` | `work_schedules:write` ¹ | write | | `unarchive_work_schedule` | `work_schedules:write` ¹ | write | | `unassign_work_schedule` | `work_schedules:write` ¹ | write | | `update_work_schedule` | `work_schedules:write` ¹ | write | ### Control horario [#time-tracking] Registro de jornada (RD-ley 8/2019): fichar entrada/salida con pausas, entradas manuales, el flujo de correcciones de fichaje, saldos y hojas de horas mensuales, el cierre mensual inalterable del registro (cerrar/reabrir/sellar, firma, validación de cadena) y sus exportaciones, además de las exportaciones de nómina. Requiere el módulo `control_horario`. *(29 tools)* | Herramienta | Scope | Categoría | | ----------------------------------- | ---------------------- | --------- | | `export_closed_register` | `time_entries:read` | read | | `get_current_time_entry_session` | `time_entries:read` | read | | `get_employee_time_balance` | `time_entries:read` | read | | `get_monthly_close_report` | `time_entries:read` | read | | `get_monthly_register_signature` | `time_entries:read` | read | | `get_monthly_time_record_close` | `time_entries:read` | read | | `get_monthly_time_sheet` | `time_entries:read` | read | | `get_team_time_balance_summary` | `time_entries:read` | read | | `get_time_correction` | `time_entries:read` | read | | `get_time_entry` | `time_entries:read` | read | | `get_time_tracking_settings` | `time_entries:read` | read | | `search_monthly_time_record_closes` | `time_entries:read` | read | | `search_time_corrections` | `time_entries:read` | read | | `search_time_entries` | `time_entries:read` | read | | `validate_time_record_chain` | `time_entries:read` | read | | `export_payroll` | `payroll_exports:read` | read | | `list_payroll_export_formats` | `payroll_exports:read` | read | | `approve_time_correction` | `time_entries:write` ¹ | write | | `clock_in` | `time_entries:write` ¹ | write | | `clock_out` | `time_entries:write` ¹ | write | | `close_monthly_time_record` | `time_entries:write` ¹ | write | | `pause_time_entry` | `time_entries:write` ¹ | write | | `record_manual_time_entry` | `time_entries:write` ¹ | write | | `reject_time_correction` | `time_entries:write` ¹ | write | | `reopen_monthly_time_record` | `time_entries:write` ¹ | write | | `request_time_correction` | `time_entries:write` ¹ | write | | `resume_time_entry` | `time_entries:write` ¹ | write | | `seal_monthly_time_record` | `time_entries:write` ¹ | write | | `update_time_tracking_settings` | `time_entries:write` ¹ | write | ### Ausencias [#absence] Ausencias: tipos, políticas (con configuración de carryover y asignaciones), solicitudes (crear/aprobar/rechazar/cancelar), saldos y el calendario de ausencias del equipo. Requiere el módulo `control_horario`. *(25 tools)* | Herramienta | Scope | Categoría | | ------------------------------------ | ----------------------- | --------- | | `get_absence_balance` | `absences:read` | read | | `get_absence_calendar` | `absences:read` | read | | `get_absence_policy` | `absences:read` | read | | `get_absence_request` | `absences:read` | read | | `get_absence_type` | `absences:read` | read | | `list_absence_policy_assignments` | `absences:read` | read | | `search_absence_balances` | `absences:read` | read | | `search_absence_policies` | `absences:read` | read | | `search_absence_requests` | `absences:read` | read | | `search_absence_types` | `absences:read` | read | | `archive_absence_policy` | `absences:write` ¹ | write | | `archive_absence_type` | `absences:write` ¹ | write | | `assign_absence_policy` | `absences:write` ¹ | write | | `configure_absence_policy_carryover` | `absences:write` ¹ | write | | `create_absence_policy` | `absences:write` ¹ | write | | `create_absence_request` | `absences:write` ¹ | write | | `create_absence_type` | `absences:write` ¹ | write | | `unarchive_absence_policy` | `absences:write` ¹ | write | | `unarchive_absence_type` | `absences:write` ¹ | write | | `unassign_absence_policy` | `absences:write` ¹ | write | | `update_absence_policy` | `absences:write` ¹ | write | | `update_absence_type` | `absences:write` ¹ | write | | `approve_absence_request` | `absences:transition` ¹ | write | | `cancel_absence_request` | `absences:transition` ¹ | write | | `reject_absence_request` | `absences:transition` ¹ | write | ### Presencia [#presence] Presencia: el panel de presencia en vivo y el estado de presencia por empleado y diario. Requiere el módulo `control_horario`. *(3 tools)* | Herramienta | Scope | Categoría | | ----------------------- | --------------- | --------- | | `get_employee_presence` | `presence:read` | read | | `get_live_presence` | `presence:read` | read | | `list_daily_presence` | `presence:read` | read | ### Festivos [#holiday] Festivos: consultar el calendario de festivos de la empresa y resolver los festivos aplicables a la región (CCAA) de un empleado. Requiere el módulo `control_horario`. *(3 tools)* | Herramienta | Scope | Categoría | | ----------------------------- | --------------- | --------- | | `get_holiday` | `holidays:read` | read | | `list_holidays` | `holidays:read` | read | | `resolve_applicable_holidays` | `holidays:read` | read | --- # Integración GoCardless (/es/payments/gocardless) <Callout type="warn"> **GoCardless todavía no está disponible.** Sus endpoints v1 y sus tools MCP **no están registrados**, así que llamarlos hoy devuelve `404 route_not_found`. Esta página documenta el **estado** de la integración y la superficie que aparecerá cuando se libere — no es una guía de uso, y nada de lo que sigue debe leerse como «esto ya se puede llamar». </Callout> GoCardless cobra por **adeudo directo SEPA**: en lugar de cargar una tarjeta, tu cliente firma un **mandato** que te autoriza a sacar dinero de su cuenta bancaria, y todo cobro posterior corre contra ese mandato. Ese modelo cambia dos cosas frente a una pasarela de tarjeta — el dinero se mueve con un calendario diferido y una ventana de garantía, y el mandato tiene vida propia: nace, se activa y se puede cancelar o caducar con independencia de cualquier cobro concreto. ## Qué significa «todavía no liberada» [#status] La única fuente de verdad es la lista de pasarelas de pago liberadas del backend (`integrations.released_providers`), que hoy contiene **solo Stripe**. Es configuración, no código, así que una pasarela se enciende sin desplegar código. Mientras GoCardless esté fuera de esa lista: * Su **bloque de rutas v1 no está registrado**. `GET /v1/gocardless/mandates` y los endpoints `/v1/gocardless-autoinvoicing/*` no existen — no están en el registro de rutas, ni en la especificación OpenAPI, ni en la referencia de API de este sitio. * Sus **tools MCP quedan filtradas** del servidor público, así que un agente ni las descubre ni las puede llamar. * La pasarela aparece como **«Próximamente»** en el marketplace de integraciones del Dashboard, y el flujo de conexión está bloqueado también en el command handler — incluso para un super-admin que se salte el middleware de módulos. * El endpoint de cuentas conectadas agnóstico de pasarela filtra sus resultados a las pasarelas liberadas, así que ninguna cuenta de GoCardless puede asomar tampoco por ahí. No falta nada ni hay nada a medio construir: las clases están **dormidas, no ausentes**. La liberación cambia una lista. ## Qué existe ya detrás del flag [#built] | Pieza | Estado | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flujo de conexión OAuth 2 | Construido. GoCardless se autentica con OAuth 2, a diferencia de MONEI | | Verificación de la firma del webhook | Construida | | Normalizador de eventos | Construido — mapea los eventos de GoCardless sobre los mismos eventos de pago internos que usa el pipeline de Stripe | | Mandatos SEPA | Construidos — se guardan con su propio ciclo de vida: `pending`, `active`, `cancelled`, `expired`, `failed`, sincronizado desde los webhooks `mandates.*` | | Cuentas conectadas por pasarela | Construidas — listar, obtener, actualizar y desconectar, replicando el modelo multi-tienda de Stripe | | Cobros y rectificativas auto-facturados | Construidos — mismas reglas de decisión, misma alta en VeriFactu que en Stripe | ### Qué eventos facturan, y cuáles no lo hacen a propósito [#events] El normalizador es más estricto que «cualquier evento de pago emite factura», y la razón es la ventana de garantía SEPA: | Evento de GoCardless | Qué produce | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payments.confirmed` | Se trata como **cobrado** → corre el flujo de auto-facturación | | `payments.charged_back`, `payments.late_failure` | Se tratan como **devolución** → flujo de factura rectificativa | | `payments.created`, `payments.submitted` | **Se ignoran a propósito** — son estados intermedios de un adeudo diferido; facturar antes de que el cobro esté garantizado sería facturar dinero que todavía puede volver atrás | | `payments.paid_out` | Hoy no tiene efecto (la conciliación de payouts de GoCardless es un seguimiento aparte) | | Cualquier otro | Se registra como evento desconocido | Por eso un cobro de GoCardless no se convierte en factura en el instante en que se envía, y es la principal diferencia de comportamiento que notarás si vienes de Stripe. ## La superficie que aparece al liberarse [#future-surface] ### Endpoints v1 [#future-endpoints] | Endpoint | Scope | | ------------------------------------------------------------------ | -------------------------------- | | `GET /v1/gocardless/mandates` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/connected-accounts` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:read` | | `PUT /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:write` | | `DELETE /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:write` | | `GET /v1/gocardless-autoinvoicing/payments` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/correctives` | `gocardless_autoinvoicing:read` | Los mandatos son de **solo lectura en la API pública**: su ciclo de vida lo gobiernan los webhooks `mandates.*`, no tus llamadas. ### Tools MCP [#future-tools] `list_gocardless_mandates`, `list_gocardless_connected_accounts`, `get_gocardless_connected_account`, `update_gocardless_connected_account`, `disconnect_gocardless_connected_account`, `list_gocardless_autoinvoiced_payments` y `list_gocardless_autoinvoiced_correctives` — una por cada endpoint de arriba, con los mismos scopes. ### Requisito de plan [#plan] La integración con GoCardless es un módulo de los planes **Empresario** y **Enterprise**, igual que las integraciones con Stripe y MONEI. Estar en el plan correcto no bastará por sí solo mientras la pasarela siga sin liberarse — tienen que cumplirse las dos condiciones. ## Equivalencias con el flujo de Stripe [#stripe-parity] Todo lo que ya sabes de [Auto-facturación con Stripe](/payments/stripe-autoinvoicing) se traslada, porque la parte específica de cada pasarela termina en el normalizador: a partir de ahí, ambas pasarelas comparten el mismo pipeline de facturación, las mismas decisiones fiscales y la misma alta en VeriFactu. | Concepto | Stripe | GoCardless | | -------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- | | Autenticación | OAuth 2 (Stripe Connect) | OAuth 2 | | Multi-tienda | `connected-accounts` por cuenta | Mismo modelo, bajo `gocardless-autoinvoicing/connected-accounts` | | Señal de «cobro con éxito» | `charge.succeeded` / `invoice.paid` | `payments.confirmed` (pasada la ventana de garantía SEPA) | | Devoluciones | `charge.refunded` → factura rectificativa | `payments.charged_back` / `payments.late_failure` → factura rectificativa | | Mandatos | No aplica | Recurso de primer nivel con su propio ciclo de vida | | Factura ordinaria o simplificada | Mismas reglas de decisión | Mismas reglas de decisión | | Ciclos de suscripción | `invoice.paid` con un `billing_reason` de suscripción | Sin rama equivalente: el normalizador solo mapea eventos `payments.*` | | Conciliación de payouts | [Soportada](/payments/payouts-reconciliation) | Hoy no cubierta | ## Qué funciona hoy de todas formas [#inbox] La [bandeja de eventos de integración](/payments/integration-events-inbox) es **agnóstica de la pasarela** y está registrada sin condiciones. Registra eventos de cualquier integración que escriba historial, incluidas las pasarelas que todavía no están liberadas — porque ocultar esas filas te dejaría sin explicación para cobros que nunca se facturaron. `provider=gocardless` es allí un valor de filtro válido desde el primer día. --- # Bandeja de eventos de integración (/es/payments/integration-events-inbox) Una pasarela de pago envía a Factuarea un evento por todo lo que ocurre en tu cuenta: un cobro con éxito, una devolución emitida, un ciclo de suscripción cobrado, un payout que llega. La mayoría de esos eventos producen algo — una factura, una rectificativa, un registro de pago. Algunos no producen nada, y cuando eso pasa la pregunta interesante siempre es la misma: **¿por qué este cobro no acabó en factura?** La **bandeja de eventos de integración** la contesta. Cada evento que Factuarea recibe queda registrado con lo que produjo y, cuando no produjo nada, con un **motivo de descarte tipado** salido de un catálogo cerrado. Sin adivinar en los logs, sin abrir un ticket de soporte: el motivo es un valor por el que puedes filtrar y, en los motivos sobre los que puedes actuar, viene con el siguiente paso y, a veces, con la posibilidad de reprocesar el evento. La bandeja es **agnóstica de la pasarela**. Registra eventos de cualquier integración que escriba historial — incluidas las pasarelas que todavía no están liberadas y los eventos históricos de una que se retire — porque ocultar esas filas te dejaría sin explicación para cobros que nunca se facturaron. La exponen tres endpoints: | Operación | Endpoint | Scope | | ------------------------------ | --------------------------------------------- | -------------------------- | | Listar eventos | `GET /v1/integrations/events` | `integration_events:read` | | Obtener un evento | `GET /v1/integrations/events/{event}` | `integration_events:read` | | Reprocesar un evento parqueado | `POST /v1/integrations/events/{event}/replay` | `integration_events:write` | La misma superficie existe como tools MCP — `list_integrations_events`, `get_integrations_event` y `replay_integrations_event` — con los mismos scopes. ## Recorrer la bandeja [#listing] Del más reciente al más antiguo, acotado a la empresa autenticada. Paginación por cursor con `limit` (de 1 a 100; 25 por defecto) y `starting_after`: ```bash curl -G https://api.factuarea.com/v1/integrations/events \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "provider=stripe" \ --data-urlencode "status=skipped" \ --data-urlencode "limit=50" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f", "object": "integration_event", "provider": "stripe", "event_type": "invoice.paid", "direction": "inbound", "status": "skipped", "discard_reason": "subscription_autoinvoicing_disabled", "discard_reason_label": "Auto-facturación de suscripciones desactivada", "is_actionable": true, "is_replayable": true, "error_message": null, "duration_ms": 412, "created_at": "2026-07-14T09:31:07Z" } ], "has_more": true, "next_cursor": "84120" } ``` Trata `next_cursor` como **opaco**: en este listado es una cadena numérica, no un UUID v7 como los cursores de los listados de documentos. Devuélvelo tal cual en `starting_after`. `discard_reason_label` llega **siempre en español**, el idioma de la interfaz del producto, sea cual sea el idioma de tu integración. Si construyes un panel en otro idioma, apoya tus propios textos en `discard_reason` — ese valor es el identificador estable y cerrado. ### Filtros [#filters] | Filtro | Valores | Notas | | ------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------- | | `provider` | `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl` | Conjunto cerrado | | `status` | `success`, `skipped`, `failure` | Conjunto cerrado | | `event_type` | texto libre, coincidencia exacta, hasta 100 caracteres | **No** es un enum — ver abajo | | `discard_reason` | uno de los veinte motivos del catálogo | Conjunto cerrado | | `is_parked` | `true` / `false` | Ver la nota de abajo | | `created_at[gte]`, `created_at[lte]` | ISO 8601 | Ventana inclusiva | **`discard_reason` es el eje cerrado; `event_type` no es un enum.** La columna `event_type` mezcla a propósito dos convenciones: las ramas instrumentadas más tarde guardan el tipo crudo de la pasarela (`charge.refunded`), mientras que las preexistentes conservan su propio valor semántico (`autoinvoice.*`). Búscalo por coincidencia exacta cuando sepas qué persigues, pero no lo modeles nunca como un conjunto cerrado — estarías modelando algo que la columna no garantiza. **`is_parked=false` no es lo mismo que omitir el parámetro.** El primero excluye los eventos parqueados; el segundo no excluye nada. Un valor fuera de su catálogo devuelve **422**, y un parámetro de consulta desconocido devuelve **400 `parameter_unknown`** en lugar de ignorarse en silencio — un filtro que se cae sin avisar te entrega una página que crees acotada y no lo está. ## El catálogo de motivos de descarte [#reasons] Veinte motivos tipados, uno por cada rama de descarte del pipeline de webhooks de las pasarelas. Cada uno declara dos decisiones de negocio que **no** son banderas decorativas: * **Accionable** — ¿puede hacer algo el titular de la cuenta? Solo los motivos accionables avisan. Avisar a alguien de un descarte que no puede resolver le enseña a ignorar la bandeja, y así es como se pierde el aviso que sí importaba. * **Parqueado** — ¿reprocesar el mismo contenido podría dar otro resultado? Solo los eventos parqueados guardan su contenido cifrado y admiten un reproceso. La regla detrás de la columna de parqueo: un evento se parquea cuando el descarte lo causó un **estado externo que puedes cambiar** (un ajuste apagado, una cuenta conectada que se desvinculó, una moneda todavía sin tipo de cambio). No se parquea cuando la causa es el **contenido del propio evento** (malformado, duplicado, de tipo no cubierto, importe cero, ciclo ya facturado) — reprocesarlo tomaría exactamente la misma rama y solo escribiría una segunda fila. De ahí el invariante: **todo motivo parqueado es accionable**, y seis de los nueve accionables se parquean. | Motivo | Qué lo provoca | Accionable | Parqueado | Qué hacer | | ------------------------------------- | ---------------------------------------------------------------------------------------- | ---------- | --------- | --------------------------------------------------------------------------------------------------------- | | `event_not_normalizable` | Evento malformado, o de un tipo que no se puede interpretar | No | No | Nada — no puedes arreglar el payload de la pasarela | | `duplicate_redelivery` | El evento ya se procesó; su efecto existe | No | No | Nada — reprocesarlo sería un no-op por deduplicación | | `connected_account_missing` | El webhook está mal configurado en la pasarela: el evento no dice a qué cuenta pertenece | **Sí** | No | Revisa en la pasarela que el webhook se envía desde la cuenta que tienes vinculada en Factuarea | | `connected_account_unknown` | La cuenta existe en la pasarela pero no está vinculada en Factuarea | **Sí** | **Sí** | Vuelve a vincular esa cuenta de la pasarela y reprocesa el evento | | `spontaneous_payment_missing_id` | El cobro no trae id, así que no hay clave de idempotencia | No | No | Nada — reprocesarlo duplicaría o volvería a fallar | | `autoinvoicing_disabled` | La auto-facturación está desactivada para esa integración | **Sí** | **Sí** | Activa la auto-facturación y reprocesa, **o** crea la factura a mano — nunca las dos cosas | | `unsupported_currency` | El tipo de cambio del Banco Central Europeo del día todavía no está disponible | **Sí** | **Sí** | Reprocesa el evento más tarde, cuando el tipo oficial del día esté publicado | | `refund_without_items` | La devolución no trae reembolsos individuales que rectificar | No | No | Nada — no hay nada que emitir | | `refund_autoinvoicing_disabled` | La rectificativa automática está desactivada para esa integración | **Sí** | **Sí** | Activa la rectificativa automática y reprocesa, **o** emite la rectificativa a mano — nunca las dos cosas | | `subscription_missing_invoice_id` | El ciclo cobrado no tiene identificador de factura | **Sí** | No | Crea a mano la factura de este ciclo; reprocesar daría el mismo resultado | | `subscription_proration_review` | Se cobró un prorrateo suelto y exige una decisión humana | **Sí** | No | Comprueba el importe del prorrateo en la pasarela y emite la factura a mano | | `subscription_not_a_cycle` | La factura de la pasarela no corresponde a un ciclo de suscripción facturable | No | No | Nada — el descarte es correcto | | `subscription_trial_skipped` | Importe cero o negativo (prueba o crédito): no hay base imponible | No | No | Nada — no hay nada que facturar | | `subscription_autoinvoicing_disabled` | La auto-facturación de suscripciones está desactivada | **Sí** | **Sí** | Activa la auto-facturación de suscripciones y reprocesa el evento | | `subscription_already_invoiced` | El ciclo ya tiene su factura | No | No | Nada — reprocesarlo sería un no-op por idempotencia | | `payout_missing_id` | El payout no trae identificador | No | No | Nada — no se puede conciliar ni reprocesar con seguridad | | `payout_connected_account_missing` | La cuenta conectada del payout no está vinculada | **Sí** | **Sí** | Vincula la cuenta conectada y reprocesa el evento | | `payment_failed` | El cobro falló en la pasarela | No | No | Nada — no hay nada que emitir ni que reintentar | | `event_type_not_covered` | Tipo de evento fuera del alcance del producto | No | No | Nada — reprocesarlo volvería a no hacer nada | | `checkout_lines_retrieve_failed` | Degradación, no descarte: la factura **sí** se emitió, con una línea única | No | No | Nada que reprocesar; revisa las líneas de la factura si te importa el desglose | Un motivo sin nada que hacer lo dice explícitamente. Once de los veinte son informativos, y el contrato no se inventa una instrucción para ellos: el endpoint de detalle devuelve `recommended_action: null` en lugar de una frase fabricada para rellenar el campo. <Callout type="warn"> **«O una, o la otra» significa una, no las dos.** Dos motivos te ofrecen dos salidas — activar el ajuste y reprocesar, o emitir el documento a mano. Son **excluyentes**. La idempotencia del reproceso va por la identidad del cobro y solo reconoce los documentos emitidos por esa misma vía automática, así que una factura que hayas creado a mano **no** lo frena. Hacer las dos cosas deja el mismo cobro con **dos facturas**, cada una numerada en su serie y dada de alta en VeriFactu — un daño fiscal que solo se deshace con una rectificativa. </Callout> ## Avisos: solo lo que puedes arreglar [#notifications] Un descarte accionable avisa a los administradores de la cuenta. Uno informativo no avisa nunca. El aviso lleva throttling: si ya existe un aviso **sin leer** de la misma empresa, la misma pasarela y el mismo motivo dentro de las últimas 24 horas, no se crea un segundo — un webhook mal configurado dispara cientos de eventos idénticos. La condición es *sin leer* a propósito: una vez lo has leído, si siguen llegando descartes, el siguiente **sí** avisa. Eso no es ruido, significa que la incidencia sigue viva. ## El parqueo y la ventana de 30 días [#retention] Cuando un motivo es parqueable, Factuarea guarda el evento crudo **cifrado en reposo**, para poder reprocesarlo más tarde. Ese contenido **nunca se devuelve** por la API — ni en el listado, ni en el detalle. Contiene datos personales de tus clientes finales y datos de pago, y existe para exactamente un propósito: hacer posible el reproceso. El contenido se **purga a los 30 días de parquearse el evento**. La fila sobrevive: su motivo, su estado, su fecha y su marca `is_parked` siguen en tu bandeja indefinidamente, porque el registro de que un cobro no produjo factura es historial que puedes necesitar mucho después de que el contenido caduque. <Callout type="info"> **Un evento que sigue con `is_parked: true` pero ya no es `is_replayable` significa exactamente una cosa: la ventana de retención se agotó.** La marca se deriva de si el contenido sigue ahí, así que cambia sola el día que corre la purga. Un reproceso intentado después devuelve 422 con el subcódigo `integration_event_payload_purged`. </Callout> ## El detalle: qué hacer a continuación [#detail] El endpoint de detalle devuelve todo lo del listado, más `recommended_action`: una frase en imperativo con el siguiente paso para ese motivo concreto, o `null` cuando el motivo es informativo. ```bash curl https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` La frase distingue a propósito los motivos reproducibles («… y reprocesa el evento») de los que no lo son («… emítela a mano»), de modo que nunca te apunta a una operación que respondería 422. Un evento de otra empresa y un evento que no existe devuelven el **mismo** 404 `resource_not_found`. El endpoint nunca revela si un id existe en otro sitio. ## Reprocesar un evento parqueado [#replay] Vuelve a procesar un evento de la pasarela que quedó parqueado, una vez que ya no está la causa que le impidió producir su efecto — has reactivado la auto-facturación, has vuelto a vincular la cuenta conectada, ya está disponible el tipo de cambio oficial del día. <Callout type="warn"> **Esta acción puede tener consecuencias fiscales reales.** Si la causa del descarte ya está resuelta, el reproceso **puede emitir una factura real**, con su número de serie y su alta en VeriFactu. No es un reintento inocuo: confírmalo con el titular de la cuenta antes de llamarlo. Por eso lleva su propio scope de escritura, `integration_events:write`, en lugar del scope de lectura de la bandeja — una credencial de solo lectura no debe poder facturar jamás. </Callout> ```bash curl -X POST https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f/replay \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Cuatro propiedades de esta operación importan más que su firma: * **No duplica facturas.** El reproceso pasa por el mismísimo control de idempotencia que el intento original, así que si ese cobro ya produjo una factura, el job se detiene solo y no crea nada. * **Es asíncrono.** `202` significa aceptado y encolado, **no** completado. El cuerpo devuelve el evento tal como está *ahora* — su `is_replayable` sigue siendo `true` —, no el resultado del reintento. El resultado aparece como un evento **nuevo** en la bandeja, así que consulta `GET /v1/integrations/events` para ver cómo acabó. * **Si la causa sigue presente, el evento se descarta otra vez** y se registra de nuevo. Es correcto, y es observable. * **No admite entrada.** Cualquier parámetro de consulta o clave del cuerpo devuelve **400 `parameter_unknown`** en lugar de ignorarse. Enviar uno significa que crees estar configurando algo del reintento — un modo, una serie, una fecha — que esta operación no soporta, y aceptarlo en silencio confirmaría esa expectativa falsa sobre una acción que puede emitir una factura. Un cuerpo vacío, o directamente ningún cuerpo, es el caso normal. ### Cuando se rechaza un reproceso [#replay-422] `is_replayable: true` es el contrato: cuando vale `true`, el reproceso **no** responde 422. Es la conjunción de tres condiciones — el evento está parqueado, todavía conserva su contenido y su motivo admite reproceso —, evaluadas en ese mismo orden por el propio handler que guarda el reproceso. Eso es lo que te permite ofrecer un botón de reintento sin adivinar. Los tres rechazos devuelven **422 `business_rule_violation`** y te dicen cuál es a través del `subcode`: | `subcode` | Qué significa | ¿Hay salida? | | ----------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------- | | `integration_event_not_parked` | El evento nunca se parqueó — o tuvo éxito, o su motivo no guarda el contenido | No, y nunca la habrá | | `integration_event_payload_purged` | Se parqueó, pero su contenido se borró al agotarse la ventana de 30 días | No — resuélvelo a mano | | `integration_event_reason_not_replayable` | Está parqueado y conserva su contenido, pero su motivo volvería a tomar exactamente la misma rama | No — sigue en su lugar la acción recomendada | ## Dónde encaja esto [#related] * [Auto-facturación con Stripe](/payments/stripe-autoinvoicing) — el flujo que produce la mayoría de los eventos que encontrarás aquí, incluidos los [ciclos de suscripción](/payments/stripe-autoinvoicing#subscriptions) cuyo ajuste está detrás de `subscription_autoinvoicing_disabled`. * [Payouts y conciliación bancaria](/payments/payouts-reconciliation) — la ingesta de payouts que hay detrás de `payout_missing_id` y `payout_connected_account_missing`. * [Modo de prueba y sandbox](/guides/test-mode) — valida tu tratamiento de la bandeja con una clave `fact_test_` antes de conectar un botón de reproceso a una credencial de producción. * [Gestión de errores](/guides/errors) — el envelope de las respuestas 400, 404 y 422 citadas más arriba. --- # Conciliar con la metadata de sistema (/es/payments/metadata-reconciliation) Todos los documentos de Factuarea llevan un objeto `metadata` de forma libre en el que puedes escribir lo que necesites. En las facturas que Factuarea emite **automáticamente desde un ciclo de suscripción de Stripe**, la plataforma escribe además un puñado de **claves de sistema** que atan la factura al cobro del que nació: qué factura de Stripe, qué suscripción, qué periodo de facturación. Esas claves son lo que hace posible la conciliación sin mantener tu propia tabla de correspondencias. Llevan escribiéndose desde hace tiempo; esta página es donde quedan documentadas. <Callout type="info"> **Alcance: ciclos de suscripción.** Estas claves las escribe el flujo que auto-emite una factura por un **ciclo de suscripción cobrado** (ver [ciclos de suscripción](/payments/stripe-autoinvoicing#subscriptions)). Los cobros sueltos auto-facturados desde `charge.succeeded` **no** las llevan hoy — para esos, correlaciona a través del [listado de cobros auto-facturados](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list), que expone los identificadores del lado del cobro. </Callout> ## Las claves de sistema [#keys] | Clave | Qué identifica | Formato | Presencia | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `stripe_invoice_id` | La factura de Stripe del ciclo cobrado | Id de Stripe, `in_…` | **Siempre** | | `billing_reason` | Por qué Stripe facturó ese ciclo | El `billing_reason` crudo de Stripe — en la práctica `subscription_create` (primer ciclo) o `subscription_cycle` (cada renovación), los dos únicos que se auto-facturan | **Siempre** | | `stripe_subscription_id` | La suscripción a la que pertenece el ciclo | Id de Stripe, `sub_…` | Opcional — se omite cuando Stripe no envía id de suscripción | | `period_start` | Primer día del periodo facturado | `YYYY-MM-DD`, **UTC** | Opcional — se omite cuando falta el timestamp del periodo | | `period_end` | Fin del periodo facturado, literal del `period_end` de Stripe — es el límite **exclusivo**, así que en un ciclo mensual es el primer día del periodo siguiente, no el último día de este | `YYYY-MM-DD`, **UTC** | Opcional — se omite cuando falta el timestamp del periodo | Las claves opcionales **no se materializan como nulas ni vacías**: cuando el valor no aplica, la clave no se escribe. Es deliberado — una clave presente con valor vacío parecería una correlación que existe pero está en blanco, y cualquier código que la leyera tendría que distinguir «sin suscripción» de «suscripción desconocida». Comprueba la presencia de la clave, no su valor. <Callout type="warn"> **Son claves de sistema. No las escribas a mano.** Son la correlación entre una factura de Factuarea y un objeto de Stripe, y las recetas de conciliación de abajo confían en ellas. Escribir tú mismo `stripe_invoice_id` en una factura que no viene al caso hace que esa factura aparezca en una conciliación a la que no pertenece, y nada lo va a señalar — `metadata` es de forma libre por diseño. Usa tus propias claves (`erp_ref`, `project_code`, …) para tus propias correlaciones. </Callout> Las claves se leen allí donde esté la factura: `metadata` forma parte del recurso de factura, y vuelve como un objeto JSON (`{}` cuando está vacío). ## Filtrar por metadata [#filter] Ocho listados v1 aceptan un filtro `metadata`: | Recurso | Endpoint | | -------------------- | ------------------------------------------------------------------------------------------------------- | | Facturas | [`GET /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.list) | | Presupuestos | [`GET /v1/quotes`](/api-reference/quotes/public-api.v1.quotes.list) | | Facturas proforma | [`GET /v1/proformas`](/api-reference/proformas/public-api.v1.proformas.list) | | Albaranes | [`GET /v1/delivery_notes`](/api-reference/delivery-notes/public-api.v1.delivery_notes.list) | | Facturas de compra | [`GET /v1/purchase_invoices`](/api-reference/purchase-invoices/public-api.v1.purchase_invoices.list) | | Facturas recurrentes | [`GET /v1/recurring_invoices`](/api-reference/recurring-invoices/public-api.v1.recurring_invoices.list) | | Productos | [`GET /v1/products`](/api-reference/products/public-api.v1.products.list) | | Proveedores | [`GET /v1/suppliers`](/api-reference/suppliers/public-api.v1.suppliers.list) | La sintaxis es `deepObject`: `metadata[clave]=valor`, un parámetro de consulta por par. * **Los pares se combinan con AND.** Dos pares devuelven los documentos que cumplen los dos. * **Coincidencia exacta** en el valor; no hay coincidencia parcial ni por prefijo. * **Hasta 50 pares** por petición; a partir de ahí devuelve `parameter_invalid_range`. * **Las claves** deben encajar en `[A-Za-z0-9_.-]` y medir entre 1 y 64 caracteres; cualquier otra cosa devuelve `parameter_invalid_enum`. * El filtro queda **fuera** del contrato `{operator, value}` de los filtros de columna, así que no existe la forma `metadata[clave][eq]`. `metadata[clave]=valor` es toda la sintaxis. <Callout type="info"> **Deja que curl codifique los corchetes.** `[` y `]` son caracteres de glob para curl y caracteres reservados en una URL. Pasa los pares con `-G --data-urlencode`, como en las recetas de abajo, y curl los codifica correctamente. Pegar un `?metadata[clave]=valor` crudo en un shell es de donde suele salir el «el filtro se está ignorando». </Callout> ## Receta: todas las facturas de una suscripción [#recipe-subscription] La conciliación que necesitas cuando un cliente te pide todas las facturas de su plan, o cuando cierras el año de un suscriptor: ```bash curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \ --data-urlencode "limit=100" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f", "object": "invoice", "number": "2026/0184", "total": "49.90", "currency": "EUR", "metadata": { "stripe_invoice_id": "in_1QRstuVWXYZabcde", "billing_reason": "subscription_cycle", "stripe_subscription_id": "sub_1QRstuVWXYZabcde", "period_start": "2026-07-01", "period_end": "2026-08-01" } } ], "has_more": false, "next_cursor": null } ``` El listado se pagina por cursor como todos los demás: sigue leyendo mientras `has_more` valga `true`, devolviendo `next_cursor` en `starting_after`. Ver [Paginación](/guides/pagination). ## Receta: las facturas de un periodo de facturación [#recipe-period] Dos pares, combinados con AND: la suscripción y el primer día del periodo. Es la consulta que responde a «¿se facturó el ciclo de julio?». ```bash curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \ --data-urlencode "metadata[period_start]=2026-07-01" ``` Como `period_start` y `period_end` son fechas exactas en **UTC**, filtra por el límite del periodo en lugar de por un rango — el valor de la metadata es el día que Stripe reporta para el ciclo, no un mes de calendario local. Para barrer un mes entero de ciclos de todas las suscripciones, quita el par de la suscripción y consulta `metadata[period_start]` a solas. <Callout type="warn"> **Filtra por `period_start`, no por `period_end`.** `period_end` es el límite superior exclusivo de Stripe: el ciclo de julio de una suscripción mensual lleva `period_start: 2026-07-01` y `period_end: 2026-08-01`. Consultar `metadata[period_end]=2026-07-31` no devuelve nada, y ese resultado vacío se parece exactamente a un ciclo que nunca se facturó. </Callout> Un array `data` vacío para un periodo que esperabas facturado es una señal real, no un fallo del filtro. Es exactamente el caso que explica la [bandeja de eventos de integración](/payments/integration-events-inbox): ábrela filtrada por `provider=stripe` y `status=skipped` y el motivo de descarte tipado te dirá si el ciclo se saltó porque la auto-facturación estaba apagada, porque el ciclo no traía importe, o por otra cosa — y si puedes reprocesarlo. ## Relacionado [#related] * [Auto-facturación con Stripe](/payments/stripe-autoinvoicing) — cómo se emiten, para empezar, las facturas que estas claves describen. * [Bandeja de eventos de integración](/payments/integration-events-inbox) — por qué un ciclo que esperabas nunca produjo factura. * [Etiquetas y campos personalizados](/guides/tags-and-custom-fields) — escribir y consultar tus **propias** claves de metadata. --- # Integración MONEI (/es/payments/monei) <Callout type="warn"> **MONEI todavía no está disponible.** Sus endpoints v1 y sus tools MCP **no están registrados**, así que llamarlos hoy devuelve `404 route_not_found`. Esta página documenta el **estado** de la integración y la superficie que aparecerá cuando se libere — no es una guía de uso. </Callout> MONEI es una pasarela de pago española que cobra con **tarjeta y Bizum**. El dinero se mueve en el momento de la captura, como en una pasarela de tarjeta y a diferencia del adeudo directo SEPA — que es la razón de que su integración tenga una forma algo distinta de la de [GoCardless](/payments/gocardless). ## Qué significa «todavía no liberada» [#status] La única fuente de verdad es la lista de pasarelas de pago liberadas del backend (`integrations.released_providers`), que hoy contiene **solo Stripe**. Es configuración, no código, así que una pasarela se enciende sin desplegar código. Mientras MONEI esté fuera de esa lista: * Su **bloque de rutas v1 no está registrado**. Los endpoints `/v1/monei-autoinvoicing/*` no existen — no están en el registro de rutas, ni en la especificación OpenAPI, ni en la referencia de API de este sitio. * Sus **tools MCP quedan filtradas** del servidor público. * La pasarela aparece como **«Próximamente»** en el marketplace de integraciones del Dashboard, y el flujo de conexión está bloqueado también en el command handler. * El endpoint de cuentas conectadas agnóstico de pasarela filtra sus resultados a las pasarelas liberadas, así que ninguna cuenta de MONEI asoma tampoco por ahí. Las clases están **dormidas, no ausentes**. La liberación cambia una lista. ## Sin recurso de mandatos, y no es una omisión [#no-mandates] GoCardless cobra por adeudo directo SEPA, así que un **mandato** — la autorización permanente del cliente para sacar dinero de su cuenta bancaria — es un objeto de primer nivel con su propio ciclo de vida, y tiene su propio endpoint y su propia tool MCP. **MONEI no usa adeudo directo SEPA.** No hay autorización permanente que modelar, así que no hay recurso `mandates`, ni estados de mandato que sincronizar, ni webhooks de mandatos. Si estás portando una integración escrita contra GoCardless, esa rama entera desaparece; no hay nada sobre lo que mapearla. ## Qué existe ya detrás del flag [#built] | Pieza | Estado | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Flujo de conexión | Construido. MONEI se autentica con una **API key**, no con OAuth 2 | | Verificación de la firma del webhook | Construida | | Normalizador de eventos | Construido — mapea los estados de pago de MONEI sobre los mismos eventos de pago internos que usa el pipeline de Stripe | | Cuentas conectadas por pasarela | Construidas — listar, obtener, actualizar y desconectar, replicando el modelo multi-tienda de Stripe | | Cobros y rectificativas auto-facturados | Construidos — mismas reglas de decisión, misma alta en VeriFactu que en Stripe | | Mandatos | **No aplica** — ver más arriba | ### Qué estados facturan, y cuáles no lo hacen a propósito [#events] MONEI informa del estado de un pago como un `status` del propio objeto de pago, y el normalizador se apoya en él: | Estado de MONEI | Qué produce | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `SUCCEEDED` | Se trata como **cobrado** → corre el flujo de auto-facturación | | `REFUNDED`, `PARTIALLY_REFUNDED` | Se tratan como **devolución** → flujo de factura rectificativa, total o parcial | | `FAILED`, `CANCELED` | Se registran como cobro fallido; no se emite nada | | `AUTHORIZED` | **Se ignora a propósito** — una autorización sin captura no es dinero cobrado, y facturarla sería facturar un cobro que quizá nunca se capture | | Cualquier otro | Se registra como evento desconocido | Un pago sin identificador se descarta antes que ninguna otra cosa: sin id no hay identidad canónica ni clave de idempotencia, así que no se podría deduplicar ni reprocesar con seguridad. ## La superficie que aparece al liberarse [#future-surface] ### Endpoints v1 [#future-endpoints] | Endpoint | Scope | | ------------------------------------------------------------- | --------------------------- | | `GET /v1/monei-autoinvoicing/connected-accounts` | `monei_autoinvoicing:read` | | `GET /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:read` | | `PUT /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:write` | | `DELETE /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:write` | | `GET /v1/monei-autoinvoicing/payments` | `monei_autoinvoicing:read` | | `GET /v1/monei-autoinvoicing/correctives` | `monei_autoinvoicing:read` | El listado de cobros lleva un filtro `origin` (`subscription` / `oneshot`) por simetría con las demás pasarelas. Si MONEI no tiene cobros de suscripción tuyos, `origin=subscription` devuelve una página vacía en lugar de un error. ### Tools MCP [#future-tools] `list_monei_connected_accounts`, `get_monei_connected_account`, `update_monei_connected_account`, `disconnect_monei_connected_account`, `list_monei_autoinvoiced_payments` y `list_monei_autoinvoiced_correctives` — una por cada endpoint de arriba, con los mismos scopes. ### Requisito de plan [#plan] La integración con MONEI es un módulo de los planes **Empresario** y **Enterprise**, igual que las integraciones con Stripe y GoCardless. Estar en el plan correcto no bastará por sí solo mientras la pasarela siga sin liberarse — tienen que cumplirse las dos condiciones. ## Equivalencias con el flujo de Stripe [#stripe-parity] La parte específica de cada pasarela termina en el normalizador: a partir de ahí, todas las pasarelas comparten el mismo pipeline de facturación, las mismas decisiones fiscales y la misma alta en VeriFactu que describe [Auto-facturación con Stripe](/payments/stripe-autoinvoicing). | Concepto | Stripe | MONEI | | -------------------------- | ----------------------------------------------------- | ----------------------------------------------------------- | | Autenticación | OAuth 2 (Stripe Connect) | API key | | Multi-tienda | `connected-accounts` por cuenta | Mismo modelo, bajo `monei-autoinvoicing/connected-accounts` | | Señal de «cobro con éxito» | `charge.succeeded` / `invoice.paid` | Estado de pago `SUCCEEDED` | | Devoluciones | `charge.refunded` → factura rectificativa | `REFUNDED` / `PARTIALLY_REFUNDED` → factura rectificativa | | Autorización sin capturar | No se factura | `AUTHORIZED`, no se factura | | Mandatos | No aplica | No aplica | | Ciclos de suscripción | `invoice.paid` con un `billing_reason` de suscripción | Sin rama equivalente en el normalizador | | Conciliación de payouts | [Soportada](/payments/payouts-reconciliation) | Hoy no cubierta | ## Qué funciona hoy de todas formas [#inbox] La [bandeja de eventos de integración](/payments/integration-events-inbox) es **agnóstica de la pasarela** y está registrada sin condiciones, así que `provider=monei` es allí un valor de filtro válido desde el primer día — incluidos los eventos históricos, que es justo la razón de que esas filas no se oculten. --- # Payouts y conciliación bancaria (/es/payments/payouts-reconciliation) Cuando conectas Stripe vía **Stripe Connect**, Stripe no transfiere cada cobro a tu banco de uno en uno: agrupa muchos cobros, resta sus comisiones y envía un único **payout** (`po_xxx`) a tu cuenta. La línea que aparece en tu extracto reza `STRIPE PAYOUT 1.234,56 €` y es el **neto** de *N* cobros menos comisiones, así que nunca casa con el total de una sola factura. Factuarea cierra ese ciclo. **Ingiere cada payout**, lo vincula con los cobros que lo componen y concilia la línea bancaria contra el payout, no contra una factura. Cuando confirmas el match, el payout, la transacción bancaria y todos los cobros subyacentes quedan marcados como conciliados en un único paso atómico. Los payouts son de **solo lectura en la API pública**: puedes listarlos e inspeccionarlos junto con su estado de conciliación, pero la conciliación en sí se hace en el Dashboard contra tu extracto Norma 43 importado. Dos endpoints v1 los exponen: * [Listar payouts de Stripe](/api-reference/stripe/public-api.v1.payouts.list) (`payouts:read`). * [Obtener un payout de Stripe](/api-reference/stripe/public-api.v1.payouts.show) (`payouts:read`). ## Ingesta de un payout [#ingestion] Cada vez que Stripe completa un payout envía un webhook **`payout.paid`** a tu endpoint de Connect. Factuarea reacciona a él: 1. Registra el payout — `connected_account_id` (`acct_xxx`), `stripe_payout_id` (`po_xxx`), los importes **neto**, **comisiones** y **bruto**, la divisa y la **fecha de llegada** prevista — con `status: ingested`. 2. Lee las **balance transactions** del payout en tu nombre (una llamada de solo lectura y paginada a Stripe) para descubrir **qué cobros** agrupa el payout y el total de comisiones. Ese desglose se guarda como la `composition` informativa. La ingesta es **idempotente en dos niveles**: por el `event.id` de Stripe (un `payout.paid` reentregado se procesa como máximo una vez) y por el `stripe_payout_id` (dos eventos distintos del mismo `po_xxx` nunca crean una fila duplicada — la unicidad está garantizada en la base de datos, incluso con webhooks concurrentes). <Callout type="info"> Si el desglose no se puede leer (un error transitorio de Stripe tras los reintentos), el payout **no** queda ingerido a medias: se reintenta el paso completo, y el `event.id` solo se marca como procesado cuando la ingesta termina con éxito. Nunca hay una fila de payout sin sus importes. </Callout> <Callout type="warn"> Para ingerir payouts debes habilitar el evento **`payout.paid`** en tu endpoint de webhook de Connect en el Stripe Dashboard. Como siempre, valida el flujo primero con una clave `fact_test_` — en el [sandbox](/guides/test-mode) la ingesta corre contra una empresa aislada con todos los efectos externos desactivados. </Callout> ## Vinculación de los cobros con el payout [#linking] Las balance transactions le dicen a Factuarea qué cobros componen el payout. Cada componente lleva su `payment_intent` (`pi_xxx`) de Stripe — el **mismo identificador** que Factuarea estampó en el `Payment` que registró cuando el cobro se auto-facturó (ver [Auto-facturación Stripe](/payments/stripe-autoinvoicing)). Con ese identificador, Factuarea encuentra los `Payment` correspondientes de tu empresa y estampa el id del payout en cada uno. Así los cobros de un payout quedan vinculados a los cobros que los produjeron — la relación que más tarde permite que la conciliación baje en cascada hasta cada cobro. La vinculación es **best-effort e idempotente**: re-vincular el mismo payout no es un error, y un componente sin `Payment` correspondiente (un cobro recibido antes de que existiera la auto-facturación, o por otra herramienta) se registra en la [bandeja de eventos de integración](/payments/integration-events-inbox) sin bloquear el resto. Un payout que llega antes de que vincules su cuenta conectada queda aparcado ahí como `payout_connected_account_missing`: vincula la cuenta y reprocesa el evento, y el payout se ingiere. El payout se ingiere igualmente — la conciliación casa por el **importe neto**, nunca exige un desglose completo de los cobros. ## Conciliación contra el extracto bancario [#reconciliation] La conciliación corre sobre tu extracto bancario **Norma 43** importado, en el Dashboard. Cuando subes un extracto, Factuarea propone matches para cada línea de abono. **Antes** de intentar casar un abono contra una factura pendiente, comprueba si la línea es un **payout**: | Señal | Regla | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Importe** | El abono bancario equivale al importe **neto** del payout (dentro de una pequeña tolerancia de redondeo). Esta es la señal dura. | | **Ventana de llegada** | La fecha valor del banco cae dentro de **±3 días** del `arrival_date` del payout (los bancos liquidan con un pequeño desfase). | | **Descripción** | Una mención `STRIPE` **suma confianza** pero nunca es requisito — la redacción varía entre bancos. | El resultado depende de cuántos payouts `ingested` encajen: * **Exactamente uno** → un match de payout **automático**. * **Más de uno** → una **sugerencia** con los candidatos, para que elijas. * **Ninguno** → la línea continúa al matching ordinario **por factura** (un abono que no es un payout debe seguir casando con una factura). Una transacción casada contra un payout queda **excluida** del matching por factura, y viceversa, de modo que la misma línea bancaria nunca se concilia dos veces. <Callout type="info"> El matching es **por divisa**. El payout se ingiere en su divisa real y solo casa con líneas bancarias en la **misma** divisa — no hay conversión (esto es conciliación contable, no una operación fiscal, así que nunca emite ni altera una factura). </Callout> ## Confirmación del match [#confirm] Cuando confirmas un match de payout en el Dashboard, Factuarea ejecuta **una única transacción atómica**: 1. La transacción bancaria se marca **conciliada** (con tipo de match `payout`). 2. El payout transiciona a **`reconciled`** (un estado terminal) y registra la referencia de la transacción bancaria en `bank_transaction_ref`. 3. Cada `Payment` vinculado al payout queda estampado con su `reconciled_at` y la referencia de la transacción bancaria. Hay guards que protegen cada paso: la transacción bancaria debe seguir `pending`, el payout debe seguir `ingested`, y los importes deben coincidir. Confirmar un payout que **ya está conciliado**, o una transacción que **ya está casada**, se rechaza sin dejar ningún estado a medias. Toda la operación es tenant-scoped: un payout o una transacción de otra empresa nunca es visible ni conciliable. ## Inspeccionar payouts en la API [#api] Lista los payouts de tu empresa con paginación por cursor, filtrados por `status` de conciliación y por ventana de fecha de llegada: ```bash curl "https://api.factuarea.com/v1/payouts?status=ingested&arrival_date[gte]=2026-01-01&limit=25" \ -H "Authorization: Bearer fact_test_…" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7e10-9c1a-1f2e3d4c5b6a", "object": "stripe_payout", "connected_account_id": "acct_1QabcDEF2ghIJklm", "stripe_payout_id": "po_1QabcDEF2ghIJklm", "amount_net": "1234.56", "fee_total": "37.04", "amount_gross": "1271.60", "currency": "EUR", "arrival_date": "2026-01-08", "status": "ingested", "reconciled_at": null, "bank_transaction_ref": null, "composition": { "components": [ { "payment_intent": "pi_3QabcDEF2ghIJklm", "charge_id": "ch_3QabcDEF2ghIJklm", "amount": 121.00, "fee": 3.50 } ], "fee_total": 37.04 } } ], "has_more": false, "next_cursor": null } ``` Cada identificador es **opaco**: * `id` es el **UUID v7** del payout — la identidad pública del recurso. * `connected_account_id` (`acct_xxx`) y `stripe_payout_id` (`po_xxx`) son **ids externos de Stripe**, no foreign keys a otros recursos de Factuarea. * `bank_transaction_ref` es el UUID (v7) de la transacción del extracto bancario conciliada — `null` mientras el payout sigue `ingested`. * `composition` referencia **ids opacos de Stripe** (`payment_intent` = `pi_xxx`, `charge_id` = `ch_xxx`), no UUIDs internos de cobro. Puede estar vacío cuando no se pudo leer el desglose. El `status` de un payout es `ingested` hasta que se concilia contra una línea bancaria, y luego `reconciled` (terminal). Obtén un payout concreto por su `id`: ```bash curl "https://api.factuarea.com/v1/payouts/0192f3a4-7b2c-7e10-9c1a-1f2e3d4c5b6a" \ -H "Authorization: Bearer fact_test_…" ``` Devuelve `404` si el payout no existe o pertenece a otra empresa. ## El evento saliente [#event] Cuando un payout se concilia, Factuarea emite el evento **`payout.reconciled`**. Su payload lleva el snapshot completo del payout (`object`) más el importe neto, la divisa y la referencia de la transacción bancaria, de modo que un receptor de webhooks pueda cerrar su propia contabilidad en el momento en que el dinero se confirma en el banco. Suscríbete a él como a cualquier otro evento — ver [Webhooks](/guides/webhooks) y [Eventos](/guides/events). No existe un scope `payouts:write`: los payouts se observan, nunca se mutan, a través de la API. El único cambio de estado — la conciliación — se dispara desde el Dashboard contra tu extracto Norma 43, y el evento es lo que notifica a tu integración. --- # Auto-facturación con Stripe (/es/payments/stripe-autoinvoicing) Cuando conectas Stripe mediante **Stripe Connect**, Factuarea puede **emitir una factura automáticamente por cada cobro exitoso**: el cobro se convierte en una factura con `status: sent`, se da de alta en VeriFactu y se le registra un `Payment`. El flujo es idempotente de extremo a extremo, así que un webhook reentregado nunca produce una factura duplicada. Lo alimentan dos flujos: * **Flujo A** — un cobro que paga una factura de Factuarea ya existente (una Checkout Session que Factuarea creó desde un enlace de pago). La factura ya existe; el cobro la marca como pagada. * **Flujo B** — un cobro espontáneo sin factura previa (un Payment Link que el comercio creó en su propio Dashboard de Stripe, o cualquier otro cobro de Connect). Factuarea crea la factura a partir del cobro. La configuración se lee y escribe a través de los endpoints v1 a nivel de empresa: * [Obtener la configuración de auto-facturación](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show) (`stripe_autoinvoicing:read`). * [Actualizar la configuración de auto-facturación](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.update) (`stripe_autoinvoicing:write`). * [Listar cobros auto-facturados](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list) (`stripe_autoinvoicing:read`). * [Listar rectificativas auto-facturadas](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list) (`stripe_autoinvoicing:read`). Si gestionas varias **tiendas** con cuentas de Stripe distintas, cada cuenta tiene su propia serie y su propia configuración — consulta [Varias tiendas](#multi-store). La auto-facturación está limitada por el módulo de **integración con Stripe** de tu plan y viene **desactivada por defecto** — actívala explícitamente con `enabled: true`. <Callout type="info"> **Qué expone la API de Stripe.** **Configuración** de auto-facturación a nivel de empresa (una comodidad heredada del modelo de tienda única — la fuente de verdad real es la configuración por cuenta conectada, ver [Varias tiendas](#multi-store)), **cuentas conectadas**, **cobros auto-facturados**, **rectificativas** y **[payouts](/payments/payouts-reconciliation)** para la conciliación bancaria. </Callout> ## Varias tiendas [#multi-store] Un negocio puede operar varias "tiendas" o líneas (una tienda física + un curso online) con **cuentas de Stripe distintas** (Stripe Connect) y querer **numeración de facturas independiente para cada una** (`TIENDA-2026-…`, `CURSOS-2026-…`). Factuarea modela cada cuenta de Stripe que conectas como una **cuenta conectada**: cada Account Link que completas **añade** una cuenta — nunca sobrescribe la anterior — y los cobros de cada cuenta se enrutan a **la serie y la configuración de esa cuenta**. Cada cuenta conectada lleva: * una **serie** (`series_id`) usada para las facturas auto-creadas a partir de sus cobros — `null` significa que se usa la **serie de facturas por defecto de la empresa**; * su **propia configuración de auto-facturación** (`autoinvoicing_enabled`, `simplified_threshold_cents`, `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`) — toda regla fiscal de esta página aplica por cuenta. Cuando llega un webhook, Factuarea resuelve la cuenta de Stripe (`acct_xxx`) a su cuenta conectada y emite la factura **en la serie de esa cuenta**, con la política fiscal de esa cuenta — de modo que dos tiendas producen facturas en dos series de numeración separadas y correctas. ### Las cuentas nuevas nacen seguras [#multi-store-defaults] Una cuenta recién conectada **no** hereda la configuración de otra cuenta: nace con los mismos valores por defecto seguros que un alta nueva — auto-facturación **desactivada**, umbral **400 €**, "exigir NIF" desactivado, devoluciones activas, suscripciones desactivadas — y sin serie (cae en la serie por defecto de la empresa hasta que le asignes una). Configúrala explícitamente antes de que emita nada. ### Endpoints v1 por cuenta [#multi-store-api] Gestiona las cuentas bajo el recurso `connected-accounts` (mismos scopes `stripe_autoinvoicing:read|write`; la identidad es el `id` de la cuenta, un UUID v7): * [Listar cuentas conectadas](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.list) (`stripe_autoinvoicing:read`). * [Obtener una cuenta conectada](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.show) (`stripe_autoinvoicing:read`). * [Actualizar una cuenta conectada](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.update) — nombre, `series_id` (envía `null` para limpiarla) y la configuración por cuenta (`stripe_autoinvoicing:write`). * [Desconectar una cuenta conectada](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.disconnect) (`stripe_autoinvoicing:write`). ```bash # Asignar la serie CURSOS y activar la auto-facturación en una tienda curl -X PUT https://api.factuarea.com/v1/connected-accounts/0192f3a4-… \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "series_id": "0192aaaa-…", "autoinvoicing_enabled": true }' ``` Desconectar una cuenta conserva sus facturas ya emitidas y su histórico; los webhooks posteriores se registran **sin** procesar. Referenciar el `id` de una cuenta que pertenece a otra empresa devuelve `404` (`connected_account_not_found`) — el aislamiento multi-tenant nunca filtra la existencia. ### El endpoint a nivel de empresa mientras tienes una tienda [#multi-store-legacy] Los [endpoints de configuración](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show) a nivel de empresa de arriba siguen válidos **mientras tienes exactamente una cuenta conectada**: el `GET` devuelve la configuración efectiva de esa única cuenta y el `PUT` hace de **proxy** hacia ella (escribe en la configuración de la cuenta, nunca una copia duplicada a nivel de empresa). En cuanto conectas una **segunda** cuenta, la configuración a nivel de empresa ya no puede responder a "¿qué cuenta?". Tanto el `GET` como el `PUT` devuelven entonces `422` (`per_account_config_required`, mensaje en español) apuntándote a los endpoints por cuenta — Factuarea **nunca escribe en dos sitios**, así que no hay divergencia entre una configuración a nivel de empresa y las cuentas. <Callout type="info"> **Una sola fuente de verdad.** La configuración por cuenta es el único lugar donde viven los ajustes. El endpoint a nivel de empresa es una comodidad que hace de proxy a la cuenta única; nunca guarda una copia separada, de modo que leer y escribir siempre coinciden. Con dos o más cuentas, usa `connected-accounts/{account}` directamente. </Callout> ## Factura ordinaria o simplificada [#decision] Una factura española necesita el **NIF** del destinatario para emitirse como factura ordinaria (F1). Un cobro B2C sin NIF es justo el caso que la normativa resuelve con una **factura simplificada (F2)**. Factuarea decide cuál emitir a partir de los datos que trae el cobro más tu política fiscal: | Situación | Factura emitida | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | Se captura un **NIF válido** en el Checkout, o el cliente resuelto ya tiene un NIF en su ficha | **Ordinaria (F1)** | | **Sin NIF**, total del cobro **igual o por debajo** del umbral y "exigir NIF" desactivado | **Simplificada (F2)** | | **Sin NIF** y (total **por encima** del umbral **o** "exigir NIF" activado) | **Revisión manual** — no se auto-emite ninguna factura | Un NIF capturado se valida contra el formato español (NIF/NIE/CIF). Un NIF con formato no válido cuenta como **sin NIF**, así que nunca se emite una F1 con datos basura. <Callout type="info"> Los cobros derivados a revisión manual **no se pierden**: quedan registrados en el log de la integración para que emitas la factura a mano. La auto-facturación continúa con el resto — un cobro en revisión nunca hace fallar el webhook. Todos aparecen listados en la [bandeja de eventos de integración](/payments/integration-events-inbox), que es donde ves qué le pasó a cada uno. </Callout> El tope legal absoluto de una factura simplificada es de **3.000 €**, garantizado por el propio dominio de facturación: un cobro sin NIF por encima de 3.000 € siempre va a revisión manual, sea cual sea el umbral configurado. ## Capturar el NIF en el Checkout [#nif-capture] Para que un cliente que *sí* tiene NIF pueda aportarlo, las Checkout Sessions que crea Factuarea (Flujo A) activan la **recogida del identificador fiscal** de Stripe. El cliente puede introducir su `es_cif`/`eu_vat` al pagar, y ese NIF encamina hacia la F1. El NIF también se lee de cualquier `checkout.session.completed` entrante: * de `customer_details.tax_ids` (el campo estándar de Stripe), y * de los **campos personalizados** de los Payment Links que el comercio construye en su propio Dashboard de Stripe — Factuarea busca un campo cuya clave parezca un identificador fiscal (`nif`, `dni`, `cif`, `vat`, `tax`). Un cobro que llega solo como `payment_intent.succeeded` (sin Checkout) no trae ningún NIF capturado, pero **aún** puede ser una F1 si el cliente se resuelve por email y ya tiene un NIF en su ficha. ## El umbral [#threshold] `simplified_threshold_cents` es el importe **en céntimos** igual o por debajo del cual un cobro sin NIF se auto-emite como factura simplificada. Su valor por defecto es **40000 (400 €)** y acepta cualquier valor en el rango **\[0, 300000]** (0–3.000 €). Un valor por defecto conservador de 400 € es deliberado: el tope de 3.000 € solo es legal en sectores tasados concretos, y Factuarea no conoce tu sector — sube el umbral únicamente si tu actividad lo permite. ```bash curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "enabled": true, "simplified_threshold_cents": 100000, "require_nif": false }' ``` Ambos campos fiscales son **opcionales**: si omites `simplified_threshold_cents` o `require_nif`, se conservan sus valores actuales. Un umbral fuera del rango devuelve `422` (`validation_error`). ## Exigir un NIF [#require-nif] `require_nif` (por defecto `false`) tiene **dos efectos coherentes**: * En las Checkout Sessions que crea Factuarea, el identificador fiscal se marca como **obligatorio** (`if_supported`), de modo que se le solicita al cliente. * En la decisión de arriba, **veta la F2**: un cobro sin NIF va a revisión manual en lugar de convertirse en factura simplificada. Actívalo cuando tu empresa no quiera nunca facturas simplificadas automáticas: cada cobro pasa entonces a tener NIF (F1) o a esperarte en revisión manual. <Callout type="warn"> Las facturas auto-emitidas son fiscalmente reales e **irreversibles** (se crea el registro de Alta de VeriFactu). Valida tu política fiscal primero con una clave `fact_test_`: en el [sandbox](/guides/test-mode) el registro de VeriFactu se crea localmente y nunca se transmite a la AEAT, así que puedes ejercitar la decisión F1/F2/revisión sin riesgo antes de pasar a producción. </Callout> ## El evento saliente [#event] Cada factura auto-creada — F1 o F2 — emite el evento [`invoice.auto_created`](/api-reference/events/public-api.v1.events.list) y un evento `payment.received`. En una factura simplificada el payload lleva `client_id: null` (sin destinatario), de modo que un receptor de webhooks puede distinguir la F1 de la F2. ## Desglose real de IVA con Stripe Tax [#vat-breakdown] Si usas **Stripe Tax**, cada cobro ya trae el desglose fiscal real por línea — el tipo, la base imponible y, cuando aplica, la causa por la que una línea está exenta o sujeta a inversión del sujeto pasivo. Factuarea **espeja ese desglose** en la factura en lugar de aplanarlo todo a un único tipo por defecto. El webhook del Checkout no incluye las líneas, así que Factuarea hace una segunda llamada de solo lectura a la API en tu nombre para recuperarlas con sus impuestos, y mapea cada línea: | Dato de Stripe Tax | Línea de la factura | | ----------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `rate.percentage` | el tipo de IVA de la línea (`vat_rate`), tal cual — nunca se recalcula | | `taxable_amount` | la base imponible de la línea (precio unitario = base ÷ cantidad) | | `taxability_reason` `zero_rated` / `product_exempt` / `customer_exempt` | línea **exenta** al 0 % | | `taxability_reason` `reverse_charge` | línea con **inversión del sujeto pasivo (ISP)** al 0 % | Así, un cobro con IVA mixto (p. ej. 21 % de consultoría + 10 % de un libro) se convierte en una factura con **dos líneas reales**, cada una a su tipo, y el desglose multi-IVA llega hasta el registro de VeriFactu. <Callout type="info"> El IVA **nunca se recalcula** — Stripe ya lo calculó, y recalcularlo introduciría desviaciones de céntimos. Factuarea toma el tipo y la base imponible directamente de Stripe Tax. </Callout> **Cuando no hay Stripe Tax** (no lo has activado en tu cuenta de Stripe) no cambia nada: cada línea recurre al **IVA por defecto** de tu empresa, igual que antes. Una empresa sin IVA por defecto configurado recurre al **0 %** — nunca a un 21 % fantasma. ## Líneas reales [#line-items] Cuando un cobro trae varias líneas (varios productos o conceptos), aparecen como **líneas reales e independientes** en la factura — cada una con su descripción, cantidad y precio — en lugar de colapsarse en una sola. Son **líneas libres** (no enlazadas a tu catálogo de productos). Esto es **ortogonal al tipo de factura**: la decisión F1/F2 de arriba elige el *tipo*, el mapeo de líneas elige las *líneas* — tanto una factura ordinaria como una simplificada obtienen las mismas líneas reales. Un cobro que llega solo como `payment_intent.succeeded` (sin Checkout Session, por lo que no hay líneas recuperables), o un cobro cuya recuperación de líneas falla tras los reintentos, **sigue produciendo una factura**: recurre a una sola línea con el IVA por defecto. La factura nunca se pierde por un detalle no esencial. Antes de emitir, Factuarea **valida el total**: el total derivado de las líneas espejadas (suma de subtotal + IVA por línea, en EUR) debe coincidir con el importe realmente cobrado, dentro de una pequeña tolerancia de redondeo (±1 céntimo por línea, mínimo ±0,05 €). Si no coincide, el cobro se deriva a **revisión manual** (`total_mismatch`) en lugar de emitir una factura cuyo total diverja del cobro real — el webhook responde correctamente igualmente. ## Cobros en otra moneda [#currency] Un cobro en una moneda distinta del EUR ya no se descarta. Factuarea lo **convierte a EUR** usando el **tipo de cambio de referencia del Banco Central Europeo (BCE)** de la fecha de pago y emite la factura **en euros** — base, IVA y total, y el registro de VeriFactu/AEAT, todo en EUR (Art. 12.1 RD 1619/2012: la cuota de IVA debe consignarse en euros). * Un cobro en **EUR** pasa intacto. * Un cobro **≠EUR** con tipo disponible se convierte; la traza de la conversión — importe y moneda originales, tipo BCE y fecha del tipo — se escribe en las notas de la factura y en el registro de integración para auditoría fiscal. * Un cobro en una moneda **sin tipo BCE** disponible **no** se auto-factura: se deriva a revisión manual y el webhook responde correctamente igualmente. Factuarea nunca inventa un tipo. La entrada aparece en la [bandeja](/payments/integration-events-inbox#reasons) como `unsupported_currency`, **aparcada**: en cuanto se publique el tipo oficial de esa fecha, reprocesar el evento emite la factura. Los tipos del BCE se cachean a diario (sin tabla de base de datos adicional), así que varios cobros ≠EUR del mismo día comparten una sola consulta del tipo. <Callout type="info"> Emitir la factura **en la moneda original** (opción B) queda intencionadamente fuera de alcance — Factuarea siempre convierte a EUR (opción A). </Callout> ## Devoluciones y facturas rectificativas [#refunds] Una factura emitida es fiscalmente **irreversible** — nunca se borra ni se anula una vez pagada. La única forma legal de deshacerla es una **factura rectificativa**. Por eso, cuando Stripe devuelve un cobro que Factuarea facturó, Factuarea cierra el ciclo fiscal por ti: el webhook `charge.refunded` genera **automáticamente una factura rectificativa enlazada** (con su propio registro R de VeriFactu), sin rectificar a mano. Lo controla `refunds_enabled` (por defecto `true`). Solo actúa mientras la auto-facturación está activada — el gating es `enabled && refunds_enabled`. Una empresa que nunca activó la auto-facturación no ve ningún cambio. | Devolución | Rectificativa emitida | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **Total** (`refunded: true`) | Una rectificativa `total` que refleja toda la factura original como líneas negativas | | **Parcial** (`amount_refunded < amount`) | Una rectificativa `partial` con una única línea negativa por el importe devuelto, al tipo impositivo de la línea original | La rectificativa se emite siempre **por diferencias** (AEAT `TipoRectificativa: I`), porque una devolución es un abono con importes negativos. Lleva el motivo de corrección `devolucion`; si la original es una factura simplificada (F2), la rectificativa se emite como **R5** automáticamente. La factura original se localiza por dos caminos — por el metadato `factuarea_invoice` del cobro (Flujo A) o por el UUID determinista derivado del `payment_intent` (Flujo B) — así que las devoluciones de cobros emitidos **antes** de que existiera esta función también se rectifican. <Callout type="info"> **La idempotencia es por devolución individual.** Stripe reenvía `charge.refunded` con el `amount_refunded` *acumulado*, pero Factuarea se basa en el id de cada devolución individual (`re_xxx`): cada una produce **como máximo una** rectificativa. Un evento reentregado, o una segunda devolución parcial, nunca abonan por duplicado. Una devolución cuya factura original no se pueda localizar, que no esté en un estado rectificable (`sent`/`paid`) o que ya esté rectificada queda registrada en la [bandeja](/payments/integration-events-inbox) para revisión manual — nunca hace fallar el webhook. </Callout> Cada rectificativa automática emite el evento [`invoice.corrective_auto_created`](/api-reference/events/public-api.v1.events.list) (que lleva la rectificativa, la factura original, el `refund_id` de origen y el `provider`), y puedes listarlas con [Listar rectificativas auto-facturadas](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list). <Callout type="warn"> Para recibir devoluciones debes habilitar el evento **`charge.refunded`** en el endpoint de webhook de Connect en el Dashboard de Stripe. Como con la auto-facturación, valida el flujo primero con una clave `fact_test_`: en el [sandbox](/guides/test-mode) el registro R de VeriFactu se crea localmente y nunca se transmite a la AEAT. </Callout> <Callout type="warn"> **Desactivarlo aparca las devoluciones, no las descarta.** Con `refunds_enabled: false`, cada devolución queda registrada en la [bandeja](/payments/integration-events-inbox#reasons) como `refund_autoinvoicing_disabled` y se conserva, cifrada, **30 días**. Dentro de esa ventana elige **una** de las dos salidas, nunca las dos: vuelve a poner `refunds_enabled` en `true` y [reprocesa](/payments/integration-events-inbox#replay) el evento, **o** emite la rectificativa a mano. Hacer ambas cosas deja la devolución con **dos rectificativas**, cada una numerada en su serie y dada de alta en VeriFactu — la idempotencia del reproceso solo reconoce las rectificativas emitidas por esa misma vía automática, así que una hecha a mano no lo frena. Pasados los 30 días el contenido se purga y emitirla a mano es la única salida que queda. </Callout> ## Ciclos de suscripción [#subscriptions] Si cobras con **Stripe Billing** en tu cuenta conectada — suscripciones recurrentes mensuales o anuales — Factuarea puede **emitir una factura automáticamente por cada ciclo cobrado**. Cada renovación que cobra Stripe produce su propia factura conforme a VeriFactu, espejando el desglose real (líneas, periodo, impuestos) que Stripe ya calculó, igual que los cobros únicos. Es un **toggle separado**, `subscription_autoinvoicing_enabled` (por defecto `false`), por encima del flag general `enabled`. **Ambos deben estar activos** para que un ciclo se facture — activar solo las suscripciones no hace nada mientras la auto-facturación está globalmente desactivada. ```bash curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "enabled": true, "subscription_autoinvoicing_enabled": true }' ``` ### Qué ciclos se facturan [#subscription-cycles] Factuarea factura el **ciclo de facturación estándar** y deriva el resto a revisión o lo ignora, según el `billing_reason` de Stripe: | `billing_reason` | Qué hace Factuarea | | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `subscription_create` (primer ciclo) | **Se factura** | | `subscription_cycle` (cada renovación) | **Se factura** | | `subscription_update`, `subscription_threshold` (prorrateos standalone) | **Revisión manual** — se registra en el log de la integración, no se auto-factura | | `manual`, `upcoming`, `quote_accept`, otros | Se ignora con un log informativo | * **Los trials no facturan.** Un ciclo cuyo `invoice.total` es **0 €** (un periodo de prueba, o un ciclo cubierto íntegramente por crédito) no emite factura; el primer cobro real tras el trial se factura con normalidad. * **Los prorrateos standalone** (un upgrade/downgrade cobrado por su cuenta, fuera del ciclo regular) **no** se auto-facturan hoy — van a revisión manual para que decidas. Aparecen en la [bandeja](/payments/integration-events-inbox#reasons) como `subscription_proration_review`, que es accionable pero **no** reprocesable: el `billing_reason` nunca cambia, así que la factura hay que emitirla a mano. El ciclo regular siguiente se factura como siempre. * **La decisión F1/F2 es la misma.** El NIF del destinatario se lee de los `customer_tax_ids` de la invoice (más la ficha del cliente resuelto); con NIF el ciclo es una factura **ordinaria (F1)**, sin él e igual o por debajo del umbral una **simplificada (F2)**, y sin él por encima del umbral (o con "exigir NIF" activado) va a revisión manual — idéntico a las reglas de la [sección de decisión](#decision). Se aplican el mismo espejo de líneas/IVA de Stripe Tax, la conversión a EUR y la validación de total. ### Cada ciclo es su propia factura [#subscription-coexistence] Un ciclo de suscripción se convierte en una **factura suelta** — **no** crea ni toca ninguna **factura recurrente** de Factuarea. Las dos son independientes: Stripe lleva la cadencia y cada `invoice.paid` produce una factura. <Callout type="warn"> **Evita la doble facturación.** Si ya modelas la suscripción del mismo cliente como una **factura recurrente** manual en Factuarea, activar la auto-facturación de suscripciones para esa suscripción de Stripe producirá **dos facturas por periodo** — una de tu plantilla recurrente y otra del ciclo de Stripe. Elige una sola fuente por cliente: detén la factura recurrente manual o deja este toggle desactivado para esas suscripciones. </Callout> ### El evento saliente [#subscription-event] Una factura de ciclo de suscripción emite un evento **distinto**, [`invoice.subscription_auto_created`](/api-reference/events/public-api.v1.events.list) (además de `payment.received`), de modo que un receptor de webhooks puede distinguir los ciclos de suscripción de los cobros únicos. Un cobro único sigue emitiendo `invoice.auto_created` como antes. El listado de cobros auto-facturados ([Listar cobros auto-facturados](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list)) expone el contexto de suscripción (`subscription_id`, `stripe_invoice_id`, `period_start`, `period_end`) para los cobros de ciclo (`null` para cobros únicos), y un filtro opcional `origin` (`subscription`/`oneshot`). Ese mismo contexto viaja también **en la propia factura**, como claves de metadata de sistema por las que puedes filtrar los listados. En [Metadata y conciliación](/payments/metadata-reconciliation) tienes la tabla completa —qué claves se escriben siempre y cuáles se omiten cuando no aplican— y dos recetas listas para usar: todas las facturas de una suscripción, y las facturas de un único periodo de facturación. <Callout type="info"> **La idempotencia es por ciclo de facturación.** Factuarea se basa en cada id de invoice de Stripe (`in_xxx`): un evento reentregado, o un segundo evento del mismo ciclo, produce **como máximo una** factura. </Callout> <Callout type="info"> **Un ciclo que llegó con el toggle apagado todavía se puede facturar — durante 30 días.** No se descarta en silencio: queda registrado en la [bandeja](/payments/integration-events-inbox#reasons) como `subscription_autoinvoicing_disabled`, con su contenido guardado cifrado. Activa `subscription_autoinvoicing_enabled` (y `enabled`) y [reprocesa](/payments/integration-events-inbox#replay) el evento dentro de esa ventana, y se emite la **factura real del ciclo** — con su número de serie y su registro VeriFactu, igual que si se hubiera facturado en su momento. Pasados los 30 días el contenido se purga, la fila permanece y la única salida es emitir la factura a mano. Los ciclos cobrados antes de que tu endpoint de Connect empezara a enviar `invoice.paid` nunca llegaron a Factuarea, así que ahí no hay nada que reprocesar. </Callout> <Callout type="warn"> Para recibir los ciclos de suscripción debes habilitar el evento **`invoice.paid`** en el endpoint de webhook de Connect en el Dashboard de Stripe. Como siempre, valida el flujo primero con una clave `fact_test_`: en el [sandbox](/guides/test-mode) el registro de Alta de VeriFactu se crea localmente y nunca se transmite a la AEAT. </Callout> ## Valores por defecto de un vistazo [#defaults] | Campo | Por defecto | Efecto del valor por defecto | | ------------------------------------ | --------------- | ----------------------------------------------------------------------------------------------------------------- | | `enabled` | `false` | Auto-facturación desactivada — no se emite nada hasta que la actives | | `simplified_threshold_cents` | `40000` (400 €) | Los cobros sin NIF de hasta 400 € se convierten en F2 | | `require_nif` | `false` | Se permiten facturas simplificadas; el identificador fiscal se ofrece pero no se exige | | `refunds_enabled` | `true` | Una devolución de Stripe genera una factura rectificativa automática (mientras la auto-facturación está activada) | | `subscription_autoinvoicing_enabled` | `false` | Los ciclos de suscripción de Stripe no se auto-facturan hasta que lo actives (requiere también `enabled`) | Con estos valores por defecto, el único cambio de comportamiento una vez activada la auto-facturación es que un cobro sin NIF de hasta 400 € se convierte en factura simplificada en lugar de esperar en revisión, y una devolución genera su factura rectificativa automáticamente. Los cobros con NIF se comportan exactamente igual que antes. Los ciclos de suscripción quedan desactivados hasta que optes por ellos con `subscription_autoinvoicing_enabled`. --- # Precios y límites de la API (/es/pricing) La API pública y el servidor MCP están **incluidos en todos los planes de pago de Factuarea**. No hay ningún complemento que contratar ni solicitud de acceso: crea una clave desde [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) y empieza a llamar a `/v1`. Lo que sí puedes pagar es **capacidad**. Tu plan otorga un tier de límites; si necesitas uno superior sin cambiar de plan, suscríbete a un [boost de capacidad](#capacity-boost). La columna de precio de abajo es el precio de ese boost, nunca el precio del acceso. ## Niveles [#niveles] | Tier | Precio del boost | Por minuto | Por mes | API keys activas | Endpoints de webhook | | ----------- | ---------------------------- | ------------- | ------------- | ---------------- | -------------------- | | **Free** | No se vende (tier del trial) | 10 rpm | 100 | 1 | 0 | | **Starter** | 4,90 € / mes | 30 rpm | 5.000 | 3 | 1 | | **Pro** | 19,90 € / mes | 300 rpm | 50.000 | 25 | 10 | | **Scale** | Sales-led | Personalizado | Personalizado | Ilimitadas | Ilimitados | Cifras verificadas contra la configuración de tiers del backend el **2026-07-31**. Para saber cómo se comportan las cuotas —ventana deslizante, cabeceras `X-RateLimit-*`, el código `429` y la estrategia de reintento— consulta [Límites de peticiones](/guides/rate-limits). ## Qué otorga ya tu plan [#qué-otorga-ya-tu-plan] | Tu plan | Tier otorgado sin coste adicional | | -------------------------------------- | --------------------------------- | | Trial (todavía sin suscripción activa) | Free | | Emprendedor | Starter | | Empresario | Pro | | Enterprise | Scale | El tier sigue al plan por sí solo: no se elige por clave ni por petición, y cambia en cuanto cambia tu plan. ## Boost de capacidad [#capacity-boost] Un boost compra un tier **estrictamente superior** al que ya otorga tu plan —por ejemplo Starter → Pro en el plan Emprendedor—. Se contrata desde [Dashboard → Developers → Upgrade](https://app.factuarea.com/settings/developers/upgrade) y se factura mensualmente. Mientras el boost está activo, todas las claves de la empresa usan el tier del boost. Como el plan ya otorga un tier, comprar uno igual o inferior al suyo se rechaza: la regla y el error que devuelve están en [Límites de peticiones → Boost de capacidad](/guides/rate-limits#capacity-boost). ## Topes que no son peticiones [#topes-que-no-son-peticiones] Hay dos límites que se cuentan por empresa, no por petición. ### API keys activas [#api-keys-activas] Una clave cuenta mientras no esté revocada ni caducada; revocar una libera hueco al momento. Crear una por encima del tope responde `422` con `code: max_api_keys_exceeded`, indicando el tier y el tope. Para superarlo, revoca una clave que ya no uses o sube de tier. ### Endpoints de webhook [#endpoints-de-webhook] Un endpoint cuenta mientras está vivo (`active` o `degraded`); uno deshabilitado o borrado no cuenta. Crear uno por encima del tope responde `422` con `code: business_rule_violation` y `subcode: max_webhook_endpoints_reached`. <Callout type="warn"> En el tier **Free** el tope es `0`, así que ya falla el primer endpoint —y lo hace con `402 addon_required` en vez del `422` de arriba, porque no hay nada que liberar—. Los webhooks requieren un plan de pago (Starter o superior) o un boost de capacidad. </Callout> ## El modo test no es un tier más barato [#el-modo-test-no-es-un-tier-más-barato] Una clave `fact_test_` lleva el **mismo tier** que tus claves de producción, así que al tráfico de sandbox se le aplican las mismas cuotas por minuto y mensual. En modo test no se exime ningún límite de peticiones. Lo que el sandbox quita es el efecto real, no la cuota: los registros de VeriFactu se crean en local y **nunca se transmiten a la AEAT**, los correos de documentos no llegan a destinatarios reales y los eventos quedan registrados pero no se entregan a tus endpoints. Tienes la lista completa en [Modo test y sandbox](/guides/test-mode). Un tope sí se comporta distinto: las claves de test viven en una empresa sandbox aparte, así que consumen el cupo de claves de esa empresa y no el de tu empresa real. ## Alto volumen [#alto-volumen] **Scale** no tiene precio publicado: los topes se acuerdan caso por caso. Escribe a [info@factuarea.com](mailto:info@factuarea.com) desde el correo asociado a tu empresa en Factuarea, con el volumen de peticiones que esperas y los endpoints que vas a usar. Lo que lleva el tier: cuotas por minuto y mensual personalizadas, claves activas y endpoints de webhook ilimitados, acuerdo de nivel de servicio y gestor de cuenta dedicado. ## Cómo ver lo que consumes [#cómo-ver-lo-que-consumes] [Dashboard → Developers → Usage](https://app.factuarea.com/settings/developers/usage) muestra tu consumo frente al tier actual. Cada respuesta de la API lleva además las cabeceras `X-RateLimit-*`, la forma más barata de detectar que te acercas al límite antes de alcanzarlo: cómo leerlas está en [Límites de peticiones](/guides/rate-limits). --- # Resumen de los SDKs (/es/sdks) Factuarea ofrece **SDKs oficiales** que envuelven toda la API REST v1 (<Stat n="operations" /> operaciones repartidas en <Stat n="resources" /> recursos) con un runtime premium para que no tengas que escribir HTTP a mano: reintentos automáticos, idempotency keys automáticas, auto‑paginación por cursor transparente, una jerarquía de errores tipada, verificación de webhooks tipada y descargas binarias (PDF). <Cards> <Card icon="<Package />" title="TypeScript / Node.js" href="https://www.npmjs.com/package/@factuarea/sdk"> `@factuarea/sdk` en npm. ESM + CommonJS dual, declaraciones de tipos completas. Código fuente: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). </Card> <Card icon="<Package />" title="PHP" href="https://packagist.org/packages/factuarea/factuarea-php"> `factuarea/factuarea-php` en Packagist. PSR‑4, basado en Guzzle, PHP 8.2+. Código fuente: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). </Card> </Cards> <Callout type="info"> **Pre‑GA (`0.x`).** Ambos SDKs están en `0.x`. La superficie pública de métodos es estable y sigue el [contrato de nomenclatura de métodos del SDK](https://github.com/factuarea), protegido por SemVer — pero mientras esté en `0.x`, las versiones minor pueden incluir cambios incompatibles hasta `1.0.0`, que coincide con la GA de la API. Cada release fija una [`Factuarea-Version`](/guides/versioning) y la envía en cada request, de modo que el comportamiento de la API se mantiene estable hasta que actualizas el SDK. </Callout> <Callout type="warn"> **Solo en el servidor.** Tu API key es un secreto. Nunca incluyas un SDK con una clave live en un navegador, app móvil o cualquier cliente público — usa el SDK desde tu backend. </Callout> ## Instalación [#instalación] <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```bash npm install @factuarea/sdk ``` Requiere **Node 20 o superior**. El SDK está construido sobre el estándar Web `fetch`, así que también funciona en Deno, Bun y Cloudflare Workers. </Tab> <Tab value="PHP"> ```bash composer require factuarea/factuarea-php ``` Requiere **PHP 8.2 o superior** con las extensiones `json` y `mbstring` (ambas incluidas en las builds estándar de PHP). </Tab> </Tabs> ## Autenticación y entornos [#autenticación-y-entornos] Pasa tu API key. **El prefijo de la clave selecciona el entorno** — no hay un flag aparte: una clave `fact_test_…` siempre se ejecuta contra el [sandbox](/guides/test-mode) aislado, y una clave `fact_live_…` contra producción. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); factuarea.environment; // "test" or "live", derived from the key prefix ``` Configuración opcional: ```ts new Factuarea({ apiKey: "fact_live_…", // required baseUrl: "https://api.factuarea.com/v1", // override for staging timeout: 60_000, // per-request ms (default 60s) maxRetries: 2, // attempts after the first try factuareaVersion: "2026-06-04", // pinned API version header defaultHeaders: {}, // extra headers on every request }); ``` </Tab> <Tab value="PHP"> ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; // The key prefix selects the environment: // fact_test_… → sandbox fact_live_… → production $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); ``` `FactuareaClient::create()` es el punto de entrada recomendado: conecta la autenticación Bearer y registra por ti el comportamiento automático de `Idempotency-Key`. Para configuración avanzada (cliente Guzzle personalizado, política de reintentos personalizada, base URL de staging) el builder generado sigue disponible: ```php use Factuarea\Sdk\Factuarea; use Factuarea\Sdk\Models\Components\Security; $factuarea = Factuarea::builder() ->setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY'))) ->setServerURL('https://api.factuarea.com/v1') ->build(); ``` </Tab> </Tabs> ## Inicio rápido [#inicio-rápido] Crea un cliente y una factura, y luego descarga su PDF. Cada operación es accesible como `<resource>.<method>` (TypeScript) o `->{resource}->publicApiV1{Resource}{Action}` (PHP) siguiendo el contrato de nomenclatura — los snippets por endpoint de la referencia de la API muestran la llamada exacta para cada operación. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Responses are the API's `{ data: … }` envelope — read the resource off `.data`. // 1. Create a client. const { data: client } = await factuarea.clients.create({ name: "Cliente Demo SL", tax_id: "B98765432", }); // 2. Create an invoice (the API computes the totals). const { data: invoice } = await factuarea.invoices.create({ client_id: client.id, series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", issued_on: "2026-06-05", due_on: "2026-07-05", lines: [ { description: "Consultoría — junio 2026", quantity: 10, unit_price: 100, tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", }, ], }); // 3. Download the PDF (a BinaryResponse, not JSON). const pdf = await factuarea.invoices.pdf(invoice.id); await import("node:fs/promises").then((fs) => fs.writeFile("invoice.pdf", pdf.toBuffer()), ); ``` </Tab> <Tab value="PHP"> ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; use Factuarea\Sdk\Models\Components; use Brick\DateTime\LocalDate; $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); // 1. Create a client. $client = $factuarea->clients->publicApiV1ClientsCreate( new Components\CreateClientRequest( name: 'Cliente Demo SL', taxId: 'B98765432', ), ); // 2. Create an invoice (the API computes the totals). $invoice = $factuarea->invoices->publicApiV1InvoicesCreate( new Components\CreateInvoiceRequest( clientId: $client->object->data->id, seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e', issuedOn: LocalDate::parse('2026-06-05'), dueOn: LocalDate::parse('2026-07-05'), lines: [ new Components\CreateInvoiceRequestLine( description: 'Consultoría — junio 2026', quantity: 10, unitPrice: 100, taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f', ), ], ), ); // 3. Download the PDF. $pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id); file_put_contents('invoice.pdf', $pdf->bytes ?? ''); ``` </Tab> </Tabs> <Callout type="info"> Ejecuta todo primero con una clave **`fact_test_`** — los efectos del sandbox (VeriFactu → AEAT, FACe, email, webhooks) están desactivados. Cuando tu flujo funcione de extremo a extremo, cambia el prefijo a `fact_live_`. La superficie de la API es idéntica en ambos. Consulta [Test mode & sandbox](/guides/test-mode). </Callout> ## Funciones en tiempo de ejecución [#funciones-en-tiempo-de-ejecución] Ambos SDKs comparten el mismo runtime escrito a mano sobre la superficie tipada generada: * **Reintentos automáticos** — los fallos transitorios (`429` y `5xx`, además de errores de red en TypeScript) se reintentan con backoff exponencial y jitter, respetando el header `Retry-After`. Los errores de cliente deterministas (p. ej. validación `422`) **nunca** se reintentan. * **Idempotencia automática** — cada mutación recibe una `Idempotency-Key` generada para que un request reintentado nunca cree un recurso por duplicado. Anúlala por llamada cuando quieras deduplicación a nivel de app. Consulta [Idempotency](/guides/idempotency). * **Auto‑paginación por cursor** — los métodos de listado devuelven un iterable que recorre todas las páginas por ti, gestionando `next_cursor` / `has_more`. Consulta [Paginar con el SDK](#paginating-with-the-sdk). * **Errores tipados** — el [envoltorio de error](/guides/errors) de la API se mapea a una jerarquía de excepciones tipada que expone `code`, `type`, `request_id` y `status`. Tu API key nunca se incluye en ningún mensaje de error. Consulta [Gestionar errores](#handling-errors). * **Verificación de webhooks** — un verificador HMAC‑SHA256 de tiempo constante que respeta la ventana de gracia de rotación de secreto. Consulta [Verificar webhooks](#verifying-webhooks). * **Descargas binarias** — los endpoints de PDF y de ficheros devuelven una respuesta binaria que conviertes en un Buffer / stream, no JSON. ## Paginación con el SDK [#paginating-with-the-sdk] <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> Los métodos de listado devuelven un `Page`, que es en sí mismo un async iterable: ```ts const page = await factuarea.invoices.list({ status: "paid", limit: 50 }); // (a) iterate every item across every page for await (const invoice of page) { console.log(invoice.id); } // (b) page by page page.data; // items on this page page.hasMore; // boolean page.nextCursor; // opaque cursor or null const next = await page.getNextPage(); // Page | null // (c) collect everything into an array const all = await page.toArray(); ``` </Tab> <Tab value="PHP"> El helper `PageIterator` transmite cada elemento a través de todas las páginas sin gestión manual del cursor: ```php use Factuarea\Sdk\Custom\Pagination\PageIterator; use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest; $pages = new PageIterator( fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList( new PublicApiV1InvoicesListRequest(startingAfter: $cursor), )->rawResponse, ); // items() yields each item as a decoded associative array. foreach ($pages->items() as $invoice) { echo $invoice['id'], PHP_EOL; } ``` </Tab> </Tabs> Consulta [Pagination](/guides/pagination) para conocer la semántica de cursor subyacente. ## Gestión de errores [#handling-errors] <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { FactuareaError, ValidationError, RateLimitError, } from "@factuarea/sdk"; try { await factuarea.invoices.create(body); } catch (error) { if (error instanceof ValidationError) { console.error(error.fields); // { tax_id: ["NIF inválido"], … } } else if (error instanceof RateLimitError) { console.error(error.retryAfter); // seconds to wait } else if (error instanceof FactuareaError) { console.error(error.code, error.requestId); } } ``` </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Models\Errors\ErrorThrowable; try { $factuarea->invoices->publicApiV1InvoicesCreate($body); } catch (ErrorThrowable $e) { $error = $e->container->error; echo $error->type->value; // e.g. "invalid_request_error" echo $error->code; // e.g. "parameter_invalid" echo $error->param; // e.g. "client_id" echo $error->requestId; // quote this to support } ``` </Tab> </Tabs> Ramifica según el `code` estable, nunca según el `message` en español orientado a personas. El catálogo completo está en [Errors](/guides/errors). ## Verificación de webhooks [#verifying-webhooks] Pasa el **cuerpo crudo del request** (no un objeto re‑serializado), el header `Factuarea-Signature` y el secreto del endpoint: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Express, with express.raw({ type: "application/json" }) on the route: app.post("/webhooks/factuarea", (req, res) => { try { const event = factuarea.webhooks.verify( req.body.toString("utf8"), req.headers[SIGNATURE_HEADER.toLowerCase()] as string, process.env.FACTUAREA_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { /* … */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier; use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException; $verifier = new WebhookVerifier(); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; try { $event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET')); // $event is the decoded, authenticated payload } catch (WebhookSignatureException $e) { http_response_code(400); } ``` </Tab> </Tabs> La verificación usa HMAC‑SHA256 con una comparación de tiempo constante y una tolerancia de timestamp configurable (5 minutos por defecto) para rechazar replays, y acepta ambas firmas durante una ventana de gracia de rotación de secreto. Consulta [Webhooks](/guides/webhooks). ## Snippets por endpoint [#snippets-por-endpoint] Cada página de la referencia de la API muestra un snippet de **TypeScript**, **PHP** y **cURL** listo para copiar para esa operación exacta, generado a partir del spec para que nunca se desvíen de la superficie real. ## Genera tu propio cliente [#generate-your-own-client] Si tu lenguaje todavía no está cubierto, o prefieres un cliente que tú controles y guardes en tu repo, el contrato canónico legible por máquina es el spec **OpenAPI 3.1** — apunta cualquier generador a él. <Callout type="info"> El spec vive en [`https://docs.factuarea.com/api/openapi`](/api/openapi). Se genera a partir del mismo backend que sirve la API, así que nunca se desvía de la superficie real. </Callout> <Tabs items="['openapi-typescript', 'Python', 'OpenAPI Generator']"> <Tab value="openapi-typescript"> ```bash npx openapi-typescript https://docs.factuarea.com/api/openapi \ -o src/factuarea.d.ts ``` </Tab> <Tab value="Python"> ```bash openapi-python-client generate \ --url https://docs.factuarea.com/api/openapi ``` </Tab> <Tab value="OpenAPI Generator"> ```bash openapi-generator-cli generate \ -i https://docs.factuarea.com/api/openapi \ -g <language> -o ./factuarea-client ``` `<language>` puede ser cualquier [generador soportado](https://openapi-generator.tech/docs/generators) — Go, Java, C#, Ruby, Rust y más. </Tab> </Tabs> Un cliente generado no incluirá el runtime del SDK oficial (reintentos, idempotencia, paginación, verificación de webhooks) — eso lo conectas tú mismo siguiendo las [guías de conceptos clave](/guides/idempotency). ## ¿Construyes con un asistente de IA? [#construyes-con-un-asistente-de-ia] Si quieres que un agente de IA opere Factuarea directamente en lugar de generar código de cliente, conéctalo al [servidor MCP](/mcp) — la API pública expuesta como tools, con autenticación OAuth y por API key. Para Claude Code, el [plugin](/mcp/claude-code-plugin) oficial `factuarea-mcp` lo configura en dos comandos. --- # PHP (/es/sdks/php) El SDK oficial de PHP es [`factuarea/factuarea-php`](https://packagist.org/packages/factuarea/factuarea-php) en Packagist — PSR-4, basado en Guzzle. Código fuente: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). Envuelve la API REST v1 con reintentos automáticos, idempotency keys, auto-paginación por cursor, una jerarquía de errores tipada y verificación de webhooks — todo ello descrito en la [introducción al SDK](/sdks). ## Instalación [#instalación] ```bash composer require factuarea/factuarea-php ``` Requiere **PHP 8.2 o superior** con las extensiones `json` y `mbstring` (ambas incluidas en las builds estándar de PHP). ## Autenticación [#autenticación] Pasa tu API key. **El prefijo de la clave selecciona el entorno** — no hay un flag aparte: una clave `fact_test_…` siempre se ejecuta contra el [sandbox](/guides/test-mode) aislado, y una clave `fact_live_…` contra producción. ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; // The key prefix selects the environment: // fact_test_… → sandbox fact_live_… → production $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); ``` `FactuareaClient::create()` es el punto de entrada recomendado: configura la autenticación Bearer y registra por ti el comportamiento automático de `Idempotency-Key`. Para configuración avanzada (cliente Guzzle personalizado, política de reintentos personalizada, base URL de staging) el builder generado sigue disponible: ```php use Factuarea\Sdk\Factuarea; use Factuarea\Sdk\Models\Components\Security; $factuarea = Factuarea::builder() ->setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY'))) ->setServerURL('https://api.factuarea.com/v1') ->build(); ``` <Callout type="warn"> **Solo en el servidor.** Tu API key es un secreto. Nunca distribuyas el SDK con una clave live en un cliente público — úsala desde tu backend. </Callout> ## Inicio rápido [#inicio-rápido] Crea un cliente y una factura, y luego descarga su PDF. Cada operación es accesible como `->{resource}->publicApiV1{Resource}{Action}`; los snippets por endpoint de la referencia de la API muestran la llamada exacta para cada operación. ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; use Factuarea\Sdk\Models\Components; use Brick\DateTime\LocalDate; $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); // 1. Create a client. $client = $factuarea->clients->publicApiV1ClientsCreate( new Components\CreateClientRequest( name: 'Cliente Demo SL', taxId: 'B98765432', ), ); // 2. Create an invoice (the API computes the totals). $invoice = $factuarea->invoices->publicApiV1InvoicesCreate( new Components\CreateInvoiceRequest( clientId: $client->object->data->id, seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e', issuedOn: LocalDate::parse('2026-06-05'), dueOn: LocalDate::parse('2026-07-05'), lines: [ new Components\CreateInvoiceRequestLine( description: 'Consultoría — junio 2026', quantity: 10, unitPrice: 100, taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f', ), ], ), ); // 3. Download the PDF. $pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id); file_put_contents('invoice.pdf', $pdf->bytes ?? ''); ``` <Callout type="info"> Ejecuta todo primero con una clave **`fact_test_`** — los efectos del sandbox (VeriFactu → AEAT, FACe, email, webhooks) están desactivados. Cuando tu flujo funcione de extremo a extremo, cambia el prefijo a `fact_live_`. Consulta [Modo de prueba y sandbox](/guides/test-mode). </Callout> ## Siguientes pasos [#siguientes-pasos] El comportamiento en runtime — reintentos, idempotencia, auto-paginación por cursor, la jerarquía de errores tipada y la verificación de webhooks — es común a ambos SDK y está documentado una sola vez en la [introducción al SDK](/sdks): * [Características de runtime](/sdks#runtime-features) * [Paginar con el SDK](/sdks#paginating-with-the-sdk) * [Manejo de errores](/sdks#handling-errors) * [Verificar webhooks](/sdks#verifying-webhooks) --- # TypeScript (/es/sdks/typescript) El SDK oficial de TypeScript es [`@factuarea/sdk`](https://www.npmjs.com/package/@factuarea/sdk) en npm — ESM + CommonJS dual con declaraciones de tipos completas. Código fuente: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). Envuelve la API REST v1 con reintentos automáticos, idempotency keys, auto-paginación por cursor, una jerarquía de errores tipada y verificación de webhooks — todo cubierto en la [introducción al SDK](/sdks). ## Instalación [#instalación] ```bash npm install @factuarea/sdk ``` Requiere **Node 20 o superior**. El SDK está construido sobre el estándar Web `fetch`, así que también funciona en Deno, Bun y Cloudflare Workers. ## Autenticación [#autenticación] Pasa tu API key. **El prefijo de la clave selecciona el entorno** — no hay un flag aparte: una clave `fact_test_…` siempre se ejecuta contra el [sandbox](/guides/test-mode) aislado, y una clave `fact_live_…` contra producción. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); factuarea.environment; // "test" or "live", derived from the key prefix ``` Configuración opcional: ```ts new Factuarea({ apiKey: "fact_live_…", // required baseUrl: "https://api.factuarea.com/v1", // override for staging timeout: 60_000, // per-request ms (default 60s) maxRetries: 2, // attempts after the first try factuareaVersion: "2026-06-04", // pinned API version header defaultHeaders: {}, // extra headers on every request }); ``` <Callout type="warn"> **Solo en el servidor.** Tu API key es un secreto. Nunca distribuyas el SDK con una clave live a un navegador, app móvil o cualquier cliente público — úsala desde tu backend. </Callout> ## Inicio rápido [#inicio-rápido] Crea un cliente y una factura, luego descarga su PDF. Cada operación es accesible como `<resource>.<method>`; los snippets por endpoint de la referencia de la API muestran la llamada exacta para cada operación. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Responses are the API's `{ data: … }` envelope — read the resource off `.data`. // 1. Create a client. const { data: client } = await factuarea.clients.create({ name: "Cliente Demo SL", tax_id: "B98765432", }); // 2. Create an invoice (the API computes the totals). const { data: invoice } = await factuarea.invoices.create({ client_id: client.id, series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", issued_on: "2026-06-05", due_on: "2026-07-05", lines: [ { description: "Consultoría — junio 2026", quantity: 10, unit_price: 100, tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", }, ], }); // 3. Download the PDF (a BinaryResponse, not JSON). const pdf = await factuarea.invoices.pdf(invoice.id); await import("node:fs/promises").then((fs) => fs.writeFile("invoice.pdf", pdf.toBuffer()), ); ``` <Callout type="info"> Ejecuta todo primero con una clave **`fact_test_`** — los efectos del sandbox (VeriFactu → AEAT, FACe, email, webhooks) están desactivados. Cuando tu flujo funcione de extremo a extremo, cambia el prefijo a `fact_live_`. Consulta [Modo de prueba y sandbox](/guides/test-mode). </Callout> ## Próximos pasos [#próximos-pasos] El comportamiento en tiempo de ejecución — reintentos, idempotencia, auto-paginación por cursor, la jerarquía de errores tipada y la verificación de webhooks — es común a ambos SDK y está documentado una sola vez en la [introducción al SDK](/sdks): * [Funcionalidades en tiempo de ejecución](/sdks#runtime-features) * [Paginar con el SDK](/sdks#paginating-with-the-sdk) * [Manejo de errores](/sdks#handling-errors) * [Verificar webhooks](/sdks#verifying-webhooks) --- # Soporte (/es/support) Esta página es la única fuente de verdad sobre cómo contactar con el equipo de la API de Factuarea y qué enviar para que podamos ayudarte rápido. ## Contacto [#contacto] <Cards> <Card icon="<Mail />" title="info@factuarea.com" href="mailto:info@factuarea.com"> El canal para todo lo relacionado con la API: dudas de integración, reportes de bugs e incidencias. </Card> </Cards> Escribe desde el email asociado a tu empresa en Factuarea. ## Qué incluir al reportar un problema [#qué-incluir-al-reportar-un-problema] Cada respuesta de la API lleva un `request_id` único (en el envoltorio de error bajo `error.request_id`, y en la cabecera de respuesta `X-Request-Id`). Es lo más útil que puedes enviarnos — nos permite correlacionar logs, métricas y trazas para investigar rápidamente. ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "El campo client_id es obligatorio.", "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA" } } ``` Un buen reporte incluye: * **`request_id`** de la llamada que falla (o varios, si es intermitente). * **HTTP status** y el `type` / `code` del envoltorio de error. * **Endpoint y método** — p. ej. `POST /v1/invoices`. * **Entorno** — `live` o `test` (el prefijo de la key que usaste, `fact_live_` o `fact_test_`). Nunca pegues el secreto de la key. * **Qué esperabas** vs. qué ocurrió, y la marca de tiempo aproximada. <Callout type="warn"> Nunca compartas el secreto de una API key en un email de soporte. Envía el `request_id` — podemos encontrar la key y la petición solo con eso. Si un secreto ha quedado expuesto, [rota o revoca la key](/guides/authentication) desde el dashboard primero. </Callout> Un asunto que ya lleve lo esencial nos ayuda a triar: ``` 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV ``` ## Acceso a la API [#acceso-a-la-api] La API pública y el servidor MCP están **incluidos en todos los planes de Factuarea** — no hay programa beta ni add-on aparte. Crea tus keys desde [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys); tu tier de rate limit se deriva de tu plan (consulta [Límites de peticiones](/guides/rate-limits)). Si tus llamadas devuelven `403 addon_not_active`, tu empresa no tiene un plan activo que incluya acceso a la API — contrata o renueva un plan desde el dashboard. ## Página de estado [#página-de-estado] Una página de estado pública (uptime e historial de incidencias) vivirá en **status.factuarea.com**. Hasta entonces, avisamos a las empresas afectadas de las incidencias y del mantenimiento planificado directamente por email a los contactos registrados de las keys. ## Changelog [#changelog] Cada cambio en `/v1` — campos nuevos, endpoints nuevos, eventos nuevos, correcciones de validación y deprecaciones — se publica en el [Changelog](/changelog/launch). Los breaking changes nunca llegan a `/v1`; solo aparecen en una futura `/v2`. Consulta [Versionado](/guides/versioning) para conocer el compromiso de estabilidad. ## Self-service primero [#self-service-primero] Antes de abrir un ticket, esto suele responder la pregunta más rápido: * [FAQ](/faq) — las preguntas de integración más habituales. * [Errores](/guides/errors) — busca tu `code` para ver la causa y la solución. * [Autenticación](/guides/authentication) — keys, scopes, rotación. * [Límites de peticiones](/guides/rate-limits) — cuotas y back-off. --- # FAQ (/faq) Short answers to the questions that come up most when building against the Factuarea public API. Each one links to the guide that covers it in full. ## Access & keys [#access--keys] ### How do I get access to the API? [#how-do-i-get-access-to-the-api] The API is **included in every Factuarea plan** — no separate add-on, no access request. Create your API key from [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) and start calling `/v1`. During the 10-day trial you already get access on the `free` tier; paid plans raise the rate-limit tier. See [Rate limits](/guides/rate-limits). ### Is this key live or test? [#is-this-key-live-or-test] Read the **prefix**: `fact_live_` operates on your real company (production), `fact_test_` on an isolated sandbox. The prefix is the single source of truth — no request parameter changes the environment. See [Test mode & sandbox](/guides/test-mode). ### I lost my API key secret. Can I recover it? [#i-lost-my-api-key-secret-can-i-recover-it] No. The backend stores only a bcrypt hash of the secret, which is shown **only once** at creation. Rotate the key from the dashboard to issue a new secret and redeploy it. See [Authentication › Rotation](/guides/authentication). ### How do I rotate a key with zero downtime? [#how-do-i-rotate-a-key-with-zero-downtime] Rotate from the dashboard: the old and new secrets both stay valid for a **grace period**, so you can roll out the new one without dropped requests. Revoke a key only when you suspect a leak — that invalidates it instantly (`401 api_key_revoked`). See [Authentication › Rotation and revocation](/guides/authentication). ## Test mode [#test-mode] ### Is test data isolated from production? [#is-test-data-isolated-from-production] Yes — structurally, not by a filter. A `fact_test_` key operates on a dedicated **sandbox company**, so resources created in test are never visible to a `fact_live_` key (and vice versa), and test fiscal numbering never touches your production series. See [Test mode › Data isolation](/guides/test-mode). ### Why aren't my webhooks firing in test mode? [#why-arent-my-webhooks-firing-in-test-mode] In test, external effects are switched off: VeriFactu → AEAT, FACe, emails and **webhook delivery** are all neutralized. Events are still recorded with `livemode: false` and are queryable via `GET /v1/events`, but they are not delivered to your endpoints. Use `POST /v1/webhook_endpoints/{id}/ping` to exercise your receiver. See [Test mode › What is switched off](/guides/test-mode). ## Documents [#documents] ### What's the difference between delete, annul and void an invoice? [#whats-the-difference-between-delete-annul-and-void-an-invoice] `DELETE /v1/invoices/{id}` only works on **drafts**. Once an invoice is issued it cannot be deleted: use `POST /v1/invoices/{id}/annul` (records a documented reason, and creates the AEAT *anulación* record when VeriFactu is enabled) or `POST /v1/invoices/{id}/void` (irreversible, records a `void_reason`, rejected if the invoice has already been corrected). See [Migrate from Holded › Intentional differences](/guides/migration-from-holded). ## Money & dates [#money--dates] ### How are monetary amounts represented? [#how-are-monetary-amounts-represented] In **euros**, with two decimal places, Stripe-style — the canonical form is a decimal string like `"1234.56"`. Parse money as a fixed decimal, never a binary float, and let the API compute totals from raw line inputs. See [Amounts & dates › Money](/guides/amounts-and-dates). ### What format do dates use? [#what-format-do-dates-use] Calendar dates such as `issued_on` and `due_on` use `YYYY-MM-DD` (for example `2026-05-15`). Timestamps such as `created` and `expires_at` are **ISO 8601 in UTC** with a `Z` suffix — e.g. `2026-05-15T10:23:18Z`. See [Amounts & dates](/guides/amounts-and-dates). ### What timezone do quotas use? [#what-timezone-do-quotas-use] The **monthly rate-limit quota** resets on day 1 at 00:00 **Europe/Madrid**, while timestamps are returned in UTC. See [Amounts & dates › Timezone for quotas](/guides/amounts-and-dates) and [Rate limits](/guides/rate-limits). ## Idempotency & retries [#idempotency--retries] ### What happens if I replay an Idempotency-Key? [#what-happens-if-i-replay-an-idempotency-key] Within the 24h TTL, the API returns the **cached** response (status, headers and body) without re-running the handler, adding the `Idempotent-Replayed: true` header. A cached `4xx` is replayed too. Reusing the key with a **different** body responds `409 idempotency_key_reused`. See [Idempotency](/guides/idempotency). ### Does an idempotent replay count against my rate limit? [#does-an-idempotent-replay-count-against-my-rate-limit] No. A key replayed within its TTL **doesn't count** against your quota. Different keys with the same payload each count, one by one. See [Idempotency › What idempotency is NOT](/guides/idempotency). ### What's the maximum Idempotency-Key length? [#whats-the-maximum-idempotency-key-length] Between **1 and 64 characters**. Any opaque unique value works (UUID v7 recommended, but UUID v4, ULID or nanoid are fine). See [Idempotency › Key format](/guides/idempotency). ## Rate limits & errors [#rate-limits--errors] ### How do I know my remaining quota? [#how-do-i-know-my-remaining-quota] Every response (including `429`) carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a `429` adds `Retry-After` with the seconds to wait. Limits depend on your key's tier. See [Rate limits](/guides/rate-limits). ### How should I retry a failed request? [#how-should-i-retry-a-failed-request] `4xx` (except `429`) → don't retry, fix the request. `429` → honor `Retry-After`. `5xx` → exponential back-off with jitter, up to 5 attempts. See [Errors › Retry strategy](/guides/errors). ### Where do I report a problem with a specific request? [#where-do-i-report-a-problem-with-a-specific-request] Grab the `request_id` from the error envelope (also in the `X-Request-Id` header) and send it to support — it lets us correlate logs, metrics and traces. See [Support](/support). --- # Absences (/guides/absences) The **absence** domain has two layers: a **configuration** layer (what can be requested and how much) and a **workflow** layer (requests, balances and the calendar). Everything is scoped by `absences:read` / `absences:write` and gated by the `control_horario` module, under `https://api.factuarea.com/v1`. ## Absence types [#types] An **absence type** is what an employee can request — holiday, sick leave, a personal day. Each type carries: whether it is **paid** (`is_paid`), whether it **requires approval** (`requires_approval`), a **measurement unit** (`days` or `hours`), a hex **color**, a **visibility** (`everyone` or `managers_only`) and a status (`active` / `archived`). The name is unique per company. A default set of Spanish types is **seeded** into every new company, so you usually start with a working catalogue. | Operation | Endpoint | | ------------------- | ---------------------------------------------------------- | | List / show | `GET /v1/absence-types`, `GET /v1/absence-types/{type}` | | Create / update | `POST /v1/absence-types`, `PATCH /v1/absence-types/{type}` | | Archive / unarchive | `POST /v1/absence-types/{type}/archive`, `.../unarchive` | ```bash curl -X POST https://api.factuarea.com/v1/absence-types \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Personal leave", "is_paid": true, "requires_approval": true, "measurement_unit": "days", "color": "#4F46E5", "visibility": "everyone" }' ``` ## Absence policies [#policies] An **absence policy** decides **how much** and **for whom**. It sets an **allowance** — `limited` (a positive number of days) or `unlimited` — an **accrual method** (`annual` or `monthly`), the set of **types** it covers, and the **employees** it is assigned to. Assigning types is a full replacement; a policy is assigned to and unassigned from employees in batches. | Operation | Endpoint | | --------------------------- | ------------------------------------------------------------------ | | List / show | `GET /v1/absence-policies`, `GET /v1/absence-policies/{policy}` | | Create / update | `POST /v1/absence-policies`, `PATCH /v1/absence-policies/{policy}` | | Assign / unassign employees | `POST /v1/absence-policies/{policy}/assign`, `.../unassign` | | List assignments | `GET /v1/absence-policies/{policy}/assignments` | | Carryover | `GET /v1/absence-policies/{policy}/carryover` | | Archive / unarchive | `POST /v1/absence-policies/{policy}/archive`, `.../unarchive` | **Carryover** exposes how much unused allowance rolls over into the next accrual period per employee. Assignment always resolves the employee inside the authenticated company, so a policy of company A is never assigned to an employee of company B. ```bash curl -X POST https://api.factuarea.com/v1/absence-policies \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Standard 22 days", "allowance": { "type": "limited", "days": 22 }, "accrual_method": "annual", "absence_type_ids": ["01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b"] }' ``` ## Requests, balances and calendar [#requests] Once types and policies exist, employees **request** absences and managers resolve them. | Operation | Endpoint | Scope | | ---------------- | ----------------------------------------------------------------- | ---------------- | | Create a request | `POST /v1/absence-requests` | `absences:write` | | Approve / reject | `POST /v1/absence-requests/{request}/approve`, `.../reject` | `absences:write` | | Cancel | `POST /v1/absence-requests/{request}/cancel` | `absences:write` | | List / show | `GET /v1/absence-requests`, `GET /v1/absence-requests/{request}` | `absences:read` | | Balances | `GET /v1/absence-balances`, `GET /v1/absence-balances/{employee}` | `absences:read` | | Team calendar | `GET /v1/absence-calendar` | `absences:read` | A **balance** is the remaining allowance per employee and type, derived from the policy accrual minus approved requests. The **calendar** returns the team's absences over a date range — the manager view of who is off and when. ```bash curl -X POST https://api.factuarea.com/v1/absence-requests \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "absence_type_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "start_date": "2026-08-01", "end_date": "2026-08-15" }' ``` <Callout type="info"> A type with `requires_approval: false` is granted on request; one with `requires_approval: true` waits for a manager to approve or reject it before it counts against the balance. </Callout> ## Typical flow [#flow] 1. Review the seeded **types**, or create your own. 2. Create **policies** with an allowance and accrual, and cover the relevant types. 3. **Assign** each policy to its employees. 4. Employees **request**; managers **approve** or **reject**. 5. Read **balances** and the **calendar**, and check **carryover** at year end. Public holidays that affect absences live in their own read-only domain — see the [overview](/guides/workforce-overview) and the [holidays reference](/api-reference/holidays/public-api.v1.holidays.list). ## Next steps [#next] * [Monthly close](/guides/monthly-time-close) — approved absences feed the monthly report. * Browse the [absence-types](/api-reference/absence-types/public-api.v1.absence-types.list), [absence-policies](/api-reference/absence-policies/public-api.v1.absence-policies.list) and [absence-requests](/api-reference/absence-requests/public-api.v1.absence-requests.create) reference. --- # Account personalization (/guides/account-personalization) Personalization controls **how your invoices look and read**: the language the generated PDF is rendered in, the PDF template that frames it, and the accent color that brands it. All three live on the authenticated company and apply to every document the API renders for you. You read the current values from the `personalization` block of `GET /v1/account`, and you change them with a single partial update on `PATCH /v1/account/personalization`. Both endpoints work the same in test mode (`fact_test_` keys) and live (`fact_live_` keys). ## The three settings [#the-three-settings] | Setting | Field | Accepted values | | ------------------------- | -------------- | -------------------------------------------------------- | | Invoice-emission language | `language` | `es`, `en`, `ca` | | PDF template | `pdf_template` | `classic`, `modern`, `minimal`, `corporative`, `premium` | | Accent color | `accent_color` | `#RRGGBB` hex, or `null` to clear | ### Invoice-emission language [#invoice-emission-language] `language` is the locale the **generated PDF** is rendered in. Set it to `en` and the invoice headings, labels and dates of every PDF you generate switch to English; `ca` renders them in Catalan; `es` (the default) in Spanish. It does not change the `message` text of API errors — those stay Spanish, as documented in the [error model](/guides/errors). ### PDF template [#pdf-template] `pdf_template` is a slug of the closed `PdfTemplate` catalog. The five system templates are `classic`, `modern` (the default), `minimal`, `corporative` and `premium`. Which ones your account may select depends on your plan — discover the allowed set with [the templates endpoint](#discovering-available-templates) rather than hardcoding it. ### Accent color [#accent-color] `accent_color` is the `#RRGGBB` hex color used to brand the PDF (headers, totals, accents). Send `null` to clear it and fall back to the template default. ## Reading the current personalization [#reading-the-current-personalization] The `personalization` block is part of the `Account` resource: ```bash curl -s https://api.factuarea.com/v1/account \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ | jq '.data.personalization' ``` ```json { "language": "es", "pdf_template": "modern", "accent_color": "#1a73e8" } ``` `language` and `pdf_template` are always present. `accent_color` is `null` when no color is configured. ## Updating personalization [#updating-personalization] `PATCH /v1/account/personalization` is a **partial update**: only the fields you send are applied, and any field you omit keeps its current value. The response is the **updated `Account` resource** — the same shape as `GET /v1/account`, including the refreshed `personalization` block. Requires the `account:write` scope. ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "en", "pdf_template": "premium", "accent_color": "#0F766E" }' \ | jq '.data.personalization' ``` Change a single setting by sending only that field: ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "language": "ca" }' ``` Clear the accent color by sending `null`: ```bash curl -s -X PATCH https://api.factuarea.com/v1/account/personalization \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "accent_color": null }' ``` <Callout type="info"> Branch on the request shape, not on order: `language`, `pdf_template` and `accent_color` are independent. Sending one never resets the other two. </Callout> ### Validation errors [#validation-errors] Each setting is validated against its closed catalog. A value outside the catalog returns `422` with the `allowed_values` for the offending field — `language` and `pdf_template` against their enum, `accent_color` against the `#RRGGBB` pattern: ```json { "error": { "type": "validation_error", "code": "validation_failed", "message": "El idioma indicado no es válido.", "param": "language", "allowed_values": ["es", "en", "ca"] } } ``` ## Discovering available templates [#discovering-available-templates] `GET /v1/account/personalization/templates` lists the PDF templates available for your account's plan (plan-aware) plus the accepted format for `accent_color`. Use it to populate a picker instead of hardcoding the catalog. Requires the `account:read` scope. ```bash curl -s https://api.factuarea.com/v1/account/personalization/templates \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ | jq '.data' ``` ```json { "object": "personalization_templates", "templates": [ { "slug": "classic", "label": "Clásica", "available": true }, { "slug": "modern", "label": "Moderna", "available": true }, { "slug": "minimal", "label": "Minimalista", "available": true }, { "slug": "corporative", "label": "Corporativa", "available": false }, { "slug": "premium", "label": "Premium", "available": false } ], "accent_color": { "format": "#RRGGBB", "example": "#1a73e8" } } ``` The `available` flag reflects your **current plan**: a `false` slug exists in the catalog but cannot be set until you upgrade. Offer only the available templates, and read `accent_color.format` to validate the color client-side before the `PATCH`. ## Scopes [#scopes] | Operation | Endpoint | Scope | | ---------------------- | ------------------------------------------- | --------------- | | Read personalization | `GET /v1/account` | `account:read` | | List templates | `GET /v1/account/personalization/templates` | `account:read` | | Update personalization | `PATCH /v1/account/personalization` | `account:write` | --- # Acting on behalf (/guides/acting-on-behalf) Once you have [managed companies](/guides/companies) under your master tenant, there are two ways to act on one of them. You can mint a [child API key](/guides/child-api-keys) bound to it — useful when you want a credential scoped to a single company. Or you can keep using your **master key** and pick the target company per request with the `X-Active-Profile` header. One master key then operates on any company in your tree, without re-authenticating or juggling a key per NIF. This page covers the header. Set the header to the public `id` (UUID v7) of the child company you want to act on. It works on **any** endpoint — invoices, clients, series, and the rest: ```bash curl https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "X-Active-Profile: 01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c" ``` When the header is present and the company is yours, the **whole request** runs against that child company's data: every read is filtered to it and every write lands on it. The request is resolved to the child company's `company_id` before rate limiting and idempotency, so each company has its own buckets. The header is **optional and additive**. Omit it and the request operates on the company your key belongs to — exactly as before. Existing integrations keep working unchanged. <Callout type="info"> The header **never** widens your key. Its `scopes`, `tier` and `environment` carry over untouched: a master key holding only `invoices:read` that targets a child company still cannot `POST /v1/invoices` there (`403 insufficient_scope`), and a `fact_test_` key stays in the sandbox regardless of the active profile. Switching profile changes **which** company you act on, never **what** you are allowed to do. The child company inherits the master's plan and add-ons. </Callout> ## Resolution and errors [#resolution] `X-Active-Profile` resolves the active company before any handler runs: | Header | Outcome | | ------------------------------------------------------------ | --------------------------------------------------------------- | | Absent or empty | The request operates on the company the key belongs to (no-op). | | Your own master company's `id` | Allowed — equivalent to omitting the header. | | A child company you own, `active` | The request operates on that child company. | | A child company you own, but `inactive` | `403 company_inactive` — reactivate it first. | | Not a valid UUID v7 | `400 parameter_invalid_uuid`, with `param: "X-Active-Profile"`. | | A company you do **not** own (another tree, or non-existent) | `404 profile_not_found`. | The `404` is **indistinguishable** whether the company belongs to another master or does not exist at all — the API never reveals that a company outside your tree exists: ```json { "error": { "type": "not_found_error", "code": "profile_not_found", "message": "El perfil de empresa indicado no existe o no pertenece a tu cuenta.", "param": "X-Active-Profile" } } ``` The `403` is different: the company **is** yours, so revealing that it is deactivated is legitimate — it is the signal to [reactivate it](/guides/companies#activate) before operating: ```json { "error": { "type": "authorization_error", "code": "company_inactive", "message": "Esta empresa está desactivada. Actívala para operar.", "param": "X-Active-Profile" } } ``` ## Which one should you use? [#which] `X-Active-Profile` and [child API keys](/guides/child-api-keys) solve different needs and coexist: * Use a **child key** to hand a narrowly scoped credential to an integration tied to one company — the credential itself is bound to that company. * Use the **header** to drive many companies from a single master key — one credential, target chosen per request. The header only switches the active company. It never changes the key's scopes, and cross-master isolation is enforced the same way as for [managing the companies](/guides/companies#scopes): a company outside your tree is never observable, returning `404` rather than `403`. --- # Amounts & dates (/guides/amounts-and-dates) Every monetary, date and time value in the public API follows a small set of fixed conventions. They are the same across every resource, so once you handle them in one place your client works everywhere. ## Money [#money] Amounts are always in **euros (EUR)** — the `currency` field is present on every document and is `"EUR"` in v1 ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)). There is no multi-currency support yet. Amounts carry **two decimal places** (cents precision). The canonical representation is a **decimal string** with exactly two decimals, Stripe-style: ```json { "price": "1234.56" } ``` <Callout type="warn"> Some resources currently emit amounts as JSON **numbers** (floats) rather than decimal strings — for example a document's `total`, `subtotal` or `unit_price` come back as `1802.9`, `968`, `100`. Write your parser to accept **both** a string and a number for any money field, and normalise to a fixed decimal type on your side (e.g. `Decimal` in Python, a big-decimal / minor-units integer in JS). Never store money as a raw binary float. </Callout> ### Let the API compute totals [#let-the-api-compute-totals] <Callout type="info"> **Do not pre-round and do not pre-compute.** Send the raw inputs of each line (`quantity`, `unit_price`, `discount`, the tax `*_id`) and let the API derive the subtotal, VAT, surcharge, retention and the grand total. The server is the single source of truth for every total — if you round line amounts yourself before sending them, your figures can drift from what the API stores. </Callout> The total of a document follows one formula across the whole API: ``` total = subtotal + total_vat + total_surcharge − total_retention ``` If you need to preview the breakdown **before** creating a document — for an order summary, a cart, or to reconcile your own figures — call `POST /v1/taxes/calculate-totals` with the lines and read back the computed `subtotal`, `total_vat`, `total_surcharge`, `total_retention` and `total` (amounts in EUR), plus a per-line breakdown in the same order: ```json { "subtotal": 250, "total_vat": 52.5, "total_surcharge": 0, "total_retention": 15, "total": 287.5, "lines": [ { "subtotal": 100, "vat_amount": 21, "surcharge_amount": 0, "retention_amount": 0, "total": 121 } ] } ``` The same per-line tax breakdown applies to every sales document: **invoices, quotes, proformas and delivery notes** all accept a per-line `retention_rate` and `surcharge_rate` (IRPF withholding and equivalence surcharge, 0–100), and their header carries the aggregated `total_vat`, `total_surcharge` and `total_retention`. The same formula holds everywhere. Each line also echoes `retention_rate` and `surcharge_rate` back in the response, so you can reconcile the breakdown line by line. <Callout type="warn"> **Equivalence surcharge follows fixed legal pairs.** When a line declares a `surcharge_rate`, it must match the VAT rate of that line according to the Spanish regime: **21% → 5.2%**, **10% → 1.4%**, **4% → 0.5%**, **0% → 0%**. An illegal pair (e.g. `tax_rate: 21` with `surcharge_rate: 1.4`) is rejected with `422` and the allowed pairs are returned in `error.allowed_values`. Send only the surcharge that the line's VAT rate admits. </Callout> ## Purchase invoices — per-line tax [#purchase-invoices--per-line-tax] A purchase invoice records what a **supplier** charged you, so each of its lines accepts a few extra fiscal qualifiers that the sales side covers in its own way. On `CreatePurchaseInvoiceRequest.lines[]` you can set: | Field | Type | Meaning | | ------------------ | -------------- | ---------------------------------------------------------------------------------- | | `retention_rate` | number (0–100) | IRPF withholding applied to the line. | | `surcharge_rate` | number | Equivalence surcharge, same legal pairs as sales. | | `vat_deductible` | boolean | Informative — flags the VAT as deductible. It does **not** change the amount paid. | | `exemption_reason` | enum or `null` | Why the line is exempt or non-subject (see below). | The per-line total follows the same shape as the sales side, with retention subtracted and surcharge added: ``` line total = subtotal + taxes − retention_amount + surcharge_amount ``` `surcharge_rate` on a purchase line follows the **same legal VAT↔surcharge pairs** as a sales line, validated server-side: **21 → 5.2**, **10 → 1.4**, **4 → 0.5**, **0 → 0**. The `0 → 0` pair is valid on the purchase side too — an exempt or zero-rated line carries a zero surcharge. An illegal pair is rejected with `422`. `exemption_reason` qualifies why a line is outside ordinary VAT. It is an enum or `null` (or absent), which means the line inherits the qualification from the invoice header, or declares none: | Value | Kind | | ---------------------------------- | ----------------------------------- | | `E1`, `E2`, `E3`, `E4`, `E5`, `E6` | Exempt (LIVA exemption cause) | | `N1`, `N2` | Non-subject (out-of-scope cause) | | `null` | Inherit from header / none declared | A purchase-invoice line carrying both a surcharge and an exemption reason: ```json { "description": "Wholesale goods", "quantity": 10, "unit_price": "50.00", "tax_rate": 21, "surcharge_rate": 5.2, "retention_rate": 0, "vat_deductible": true, "exemption_reason": null } ``` ```bash curl -s -X POST https://api.factuarea.com/v1/purchase_invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "supplier_id": "01931b3e-...s01", "received_on": "2026-03-15", "lines": [ { "description": "Exempt service", "quantity": 1, "unit_price": "200.00", "tax_rate": 0, "surcharge_rate": 0, "exemption_reason": "E1" } ] }' | jq '.data | {subtotal, total_vat, total_surcharge, total_retention, total}' ``` <Callout type="info"> **Cents in tax reports.** Aggregated fiscal endpoints (Modelo 303 / 347 via `/v1/tax_reports/*`) return their amounts as **integer cents**, not EUR decimals — e.g. an accumulated taxable base of `25000` means `250.00 €`. This is documented per field in the spec; treat tax-report figures as minor units and divide by 100 only for display. </Callout> ## Dates [#dates] Calendar dates (no time component) use **`YYYY-MM-DD`** — the [ISO-8601 / RFC 3339](https://en.wikipedia.org/wiki/ISO_8601) full-date form. This covers fields such as `issued_on`, `due_on`, `paid_on`, `valid_until`, `delivery_date`, `received_on`, `start_on` and `end_on`: ```json { "issued_on": "2026-03-15", "due_on": "2026-04-14", "paid_on": "2026-03-20" } ``` Send dates in the same format. There is no time and no timezone on a date — it is the calendar day as recorded for the document. ## Timestamps [#timestamps] Instant-in-time fields (audit and lifecycle metadata such as `created_at`, `updated_at`, `signed_at`, `last_delivery_at`) use full **ISO-8601 / RFC 3339** date-time strings. Most are emitted in **UTC** with a `Z` suffix: ```json { "created_at": "2026-05-15T10:34:21Z" } ``` Some timestamps carry an explicit Europe/Madrid offset instead (`+01:00` in winter, `+02:00` in summer): ```json { "created_at": "2026-04-15T10:31:05+02:00" } ``` <Callout type="warn"> Both forms are valid ISO-8601 and denote the same kind of value: an exact instant. **Parse the offset** — do not assume the string is always UTC. A proper ISO-8601 parser (`Instant.parse`, `datetime.fromisoformat`, `new Date(...)`, `Carbon::parse`) handles `Z` and `±hh:mm` identically and normalises to the absolute instant. </Callout> ## Timezone for quotas [#timezone-for-quotas] The **monthly** rate-limit quota resets on **day 1 of each calendar month at `00:00` Europe/Madrid** (CET/CEST), not UTC. The per-minute quota is a sliding window and the `X-RateLimit-Reset` header is a **UNIX timestamp** (seconds since the epoch, timezone-independent). See [Rate limits](/guides/rate-limits) for the full window semantics. Whenever the API needs a single civil-calendar reference for a business boundary — fiscal periods, the monthly quota reset — that reference is **Europe/Madrid**. ## Quick reference [#quick-reference] | Value | Format | Example | | ----------------------- | -------------------------------------------------------------- | ------------------------ | | Money | EUR, two decimals — decimal string (some fields emit a number) | `"1234.56"` / `1802.9` | | Currency | ISO 4217, always `EUR` in v1 | `"EUR"` | | Tax-report amounts | Integer **cents** (minor units) | `25000` → 250.00 € | | Date | `YYYY-MM-DD` (ISO-8601 full-date) | `"2026-03-15"` | | Timestamp | ISO-8601 date-time, usually UTC `Z`, sometimes `±hh:mm` | `"2026-05-15T10:34:21Z"` | | Quota / fiscal calendar | Europe/Madrid civil time | day 1, `00:00` CET/CEST | --- # Annul or correct (/guides/annul-vs-correct) An issued invoice cannot be edited. Everything that looks like editing one is really one of four different operations, each with its own preconditions and its own consequence at the tax authority. This page is the decision table, and the reason behind each branch. ## When this applies [#when] Whenever something is wrong with an invoice and you need to undo it. **The invoice's current status narrows the legal options; where more than one is legal, your intent decides.** How serious the mistake is never enters into it. | Invoice status | Number assigned? | Operation | Consequence | | --------------------------------------------------------------------- | ---------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------- | | `draft`, and nothing about it is worth keeping | No | **Delete** — `DELETE /v1/invoices/{id}` | The record disappears. Nothing was ever fiscal. | | `draft`, but you want the attempt on record | No | **Cancel** — change status to `cancelled` | The draft is retired but kept. | | `sent`, `overdue` — the invoice should never have been issued | Yes | **Annul** — `POST /v1/invoices/{id}/annul` | The invoice stops being collectable and an annulment is declared to the AEAT. | | `sent`, `paid` — the invoice was right to exist, its content is wrong | Yes | **Corrective** — `POST /v1/invoices/{id}/corrective` | A new fiscal document referencing the original. | | `sent`, but only the delivery mark was wrong | Yes | **Clear delivery mark** — `POST /v1/invoices/{id}/unsend` | The delivery timestamp is cleared. The invoice stays issued. | Three rules make the table unambiguous: **A numbered invoice is never physically deleted.** Deletion requires status `draft` (or a `cancelled` that came from a draft) **and** a number that is still the draft placeholder. Any invoice that consumed a number from its series is protected by fiscal soft-delete; the way to retire it is annulment ([`BR-INV-002`](#traceability), art. 29.4 of the Spanish General Tax Act on the duty to preserve documents with tax relevance). **A paid invoice is closed.** `paid` is terminal: its output VAT has been or will be declared for the period and the collection is identified, so annulling it would break traceability and distort the VAT returns. The canonical path is a corrective ([`BR-INV-023`](#traceability)). **On `sent`, both are legal — so ask what went wrong.** A corrective is allowed on `sent` or `paid` ([`BR-INV-001`](#traceability)) and an annulment on `sent` or `overdue` ([`BR-INV-003`](#traceability)), so `sent` is the one status where the API will accept either. The status cannot decide for you; the question does: * **The invoice should never have existed** — the order was cancelled, it went to the wrong customer, it duplicates another one → **annul**. * **The invoice was right to issue but its content is wrong** — wrong amount, wrong tax rate, wrong recipient details, a partial return → **corrective**. Reach for annulment on a merely wrong amount and you declare an `ANULACION` to the AEAT and burn the number for nothing; the corrective was the clean path and it stays available on `sent`. ## Cancel is not annul [#cancel] They are different acts, and the domain keeps them apart on purpose. **Cancelling** retires a *draft* — a document that is not yet fiscally binding. It is only available from `draft`, and `cancelled` is terminal: a cancelled draft cannot be revived, you create a new one ([`BR-INV-012`](#traceability)). **Annulling** retires an *issued* invoice. It is only available from `sent` or `overdue`. It is not a delete: the invoice stays in the ledger, in status `annulled`, and if the company is on VeriFactu the annulment is itself declared. Trying to cancel an issued invoice, or to annul a draft, answers `422` with an invalid-transition error. That status code is deliberate: this is a business-rule violation, not a permissions problem. ## Correctives cannot be annulled [#corrective-annul] A corrective invoice is never annulled. If a corrective itself is wrong, you issue a **new corrective of the original invoice** ([`BR-INV-003`](#traceability)). Attempting it answers `422`. The reasoning is that a corrective's whole meaning is "this document modifies that one". Annulling the modification would leave the original in an ambiguous state at the tax authority, where both documents are already registered. ## Unsend undoes a delivery mark, not an issue [#unsend] `unsend` exists for one specific mistake: marking an invoice as delivered when it was not. It clears the delivery timestamp and **keeps the status at `sent`**. The series number, the registration at the AEAT and the frozen snapshots are all untouched, because an issued invoice is immutable ([`BR-INV-030`](#traceability), RD 1007/2023). Two properties matter for integrations: * It is **idempotent**. Calling it again when the timestamp is already cleared is a controlled no-op, never a `500`. * It is strictly scoped to `sent`. On a `paid`, `annulled`, `cancelled` or scheduled invoice it answers `422` — never `403`. The status transition matrix contains no path from `sent` back to `draft` by any route, including the generic status-change endpoint. There is no "un-issue". ## What the API sends [#api] **Check first.** [`GET /v1/invoices/{id}/can-annul`](/api-reference/invoices/public-api.v1.invoices.can_annul) (scope `invoices:read`) tells you the answer before you commit, including whether the annulment will produce an extra VeriFactu record: ```bash curl https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/can-annul \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ```json { "data": { "can_annul": false, "reasons": ["La factura está pagada."], "will_create_verifactu": false, "info": [] } } ``` **Then annul.** [`POST /v1/invoices/{id}/annul`](/api-reference/invoices/public-api.v1.invoices.annul) (scope `invoices:void`) records the reason: ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/annul \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"reason": "El cliente cancela el pedido tras la emisión"}' ``` <Callout type="info"> `POST /v1/invoices/{id}/void` reaches the same domain operation under the external name the API contract uses for the state (`voided`). Prefer `annul` when you want the reason recorded; a second call on an already-annulled invoice answers `422` either way. </Callout> For the corrective path — payload, R-codes, line inheritance — see [Corrective invoices](/guides/corrective-invoices). ## What appears on the PDF [#pdf] Annulment does not rewrite the original document. The invoice keeps its number, its frozen recipient and issuer data and its QR block; what changes is its status in the ledger and the fact that a second declaration now exists at the AEAT. Deleting a draft removes the document altogether — but a draft never had a definitive number, a frozen snapshot or a QR to begin with, which is exactly why deletion is safe there and nowhere else. `unsend` changes nothing on the printed document. It only clears a delivery timestamp; the invoice does not become editable again ([`BR-INV-030`](#traceability)). ## What reaches the AEAT [#aeat] **Annulment** of an invoice from a VeriFactu company produces a second billing record of kind `ANULACION`, chained to the company's latest record and referring to the original registration ([`BR-VFC-014`](#traceability)). It is created asynchronously, after the transaction commits, so the invoice reaches `annulled` in your database before the declaration is transmitted. Poll the record if you need to confirm the AEAT accepted it — see [VeriFactu submission states](/guides/verifactu-submission-states). There is one subtlety with real consequences. If the **original registration was never accepted** — it is rejected, errored, or still pending — the annulment must declare explicitly that no previous record exists at the AEAT. The system derives that flag from the registration's status at the moment the annulment is created and persists it as a snapshot, so a later change of the original's status does not desynchronise the XML that was already transmitted. Without the flag, the AEAT refuses the annulment outright with "the billing record does not exist" ([`BR-VFC-026`](#traceability)). **Cancelling and deleting a draft** reach the AEAT in no way at all: a draft was never declared. **Correctives** are ordinary fiscal documents and produce their own registration, exactly like any other invoice. If the company is not on VeriFactu, annulment still works and simply produces no declaration. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-INV-001` — a corrective requires an original in `sent` or `paid`; that overlap with annulment on `sent` is why intent, not status, decides there. * `BR-INV-002` — fiscal soft-delete: a numbered invoice is never physically deleted. * `BR-INV-003` — annulment is limited to issued invoices; correctives are never annulled. * `BR-INV-012` — cancellation is limited to drafts and is terminal. * `BR-INV-023` — `paid` is a closed state; correct it with a corrective, never an annulment. * `BR-INV-030` — `unsend` clears the delivery mark, keeps the invoice issued, is idempotent, and answers `422` rather than `403`. * `BR-VFC-014` — the annulment record kind and the chain it belongs to. * `BR-VFC-026` — the "no previous record" flag on the annulment of a registration that was never accepted. Also derived from the invoice status machine documented alongside those rules, which is the source of truth for the transitions quoted in [When this applies](#when). --- # API keys (self-service) (/guides/api-keys) Beyond the developer dashboard, Factuarea exposes the full lifecycle of your API keys over the public v1 API, so you can provision and rotate credentials programmatically. Five endpoints under `/v1/account/api-keys` cover listing, creating, retrieving, rotating the secret and revoking — all scoped to the authenticated company. | Operation | Endpoint | Scope | | ----------------- | --------------------------------------------------- | --------------- | | List keys | `GET /v1/account/api-keys` | `account:read` | | Create a key | `POST /v1/account/api-keys` | `account:write` | | Retrieve a key | `GET /v1/account/api-keys/{api_key}` | `account:read` | | Rotate the secret | `POST /v1/account/api-keys/{api_key}/rotate_secret` | `account:write` | | Revoke a key | `POST /v1/account/api-keys/{api_key}/revoke` | `account:write` | `{api_key}` is the key's `id` — an opaque UUID v7, **not** its prefix or secret. See the full schemas in the [API Reference](/api-reference/account/public-api.v1.account.api_keys.list). ## List your keys [#list-your-keys] `GET /v1/account/api-keys` returns your keys with [cursor pagination](/guides/pagination). Each key exposes its `prefix`, `scopes`, `tier`, `environment` and lifecycle timestamps — never the secret. ```bash curl https://api.factuarea.com/v1/account/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ```json { "data": [ { "object": "api_key", "id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f", "name": "Production sync", "prefix": "fact_live_8KqW3pXn", "scopes": ["invoices:read", "invoices:write"], "tier": "scale", "environment": "live", "active": true, "revoked": false, "last_used_at": "2026-06-23T18:04:11Z", "expires_at": null, "revoked_at": null } ], "has_more": false, "next_cursor": null } ``` `prefix` is the first chars of the key — safe to log, it does **not** authenticate. Use it to recognize a key in your own dashboards without ever storing the secret. ## Create a key [#create] `POST /v1/account/api-keys` mints a new key and returns its plaintext `secret` **exactly once**. Store it the moment you receive it — there is no endpoint to read it back later. ```bash curl -X POST https://api.factuarea.com/v1/account/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Reporting export", "scopes": ["invoices:read", "pdfs:read"], "environment": "test" }' ``` Response (`201`): ```json { "data": { "object": "api_key", "id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162", "name": "Reporting export", "prefix": "fact_test_1N0Fnyhh", "secret": "fact_test_1N0FnyhhR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "pdfs:read"], "tier": "scale", "environment": "test" } } ``` <Callout type="warn"> The `secret` field appears **only** in this `201` response (and after a rotation). It is never returned by the list, retrieve or any other endpoint. If you lose it you must rotate the key. Persist it to a secret manager immediately — never to a log or a repository. </Callout> ### Request body [#request-body] | Field | Required | Notes | | ------------- | -------- | ------------------------------------------------------------------------------------------ | | `name` | yes | Human-readable label (1–120 chars). | | `scopes` | yes | One or more [scopes](/guides/authentication#scopes) from the closed catalog. At least one. | | `environment` | no | `live` (default) or `test`. See [below](#environment). | | `expires_at` | no | Future ISO 8601 instant after which the key stops authenticating. | | `allowed_ips` | no | Optional list of allowed IPs / CIDR ranges (IPv4, IPv6, `/N`). | The `tier` is **derived from your company plan** (or from an active [capacity boost](/guides/rate-limits#capacity-boost) when higher) — it is *not* settable from the body. If you send a `tier`, it is ignored. Requesting a scope outside the closed catalog, or a scope above your plan, returns `422` with [per-field errors](/guides/errors). ## The environment field [#environment] Every key belongs to one of two environments, fixed at creation and visible on the key object: | `environment` | Prefix | Operates on | | ------------- | ------------ | ------------------------------------------------------------------------------- | | `live` | `fact_live_` | Your real company, with real side effects (VeriFactu → AEAT, emails, webhooks). | | `test` | `fact_test_` | An isolated sandbox company with external effects switched off. | Pass `environment: test` when creating a key to mint a sandbox credential; omit it for a live key. The prefix mirrors the environment, so you can tell them apart without decoding the key. See [Test mode & sandbox](/guides/test-mode) for what is switched off in `test`. ## Rotate the secret [#rotate] `POST /v1/account/api-keys/{api_key}/rotate_secret` generates a fresh `prefix` + `secret` and returns the new secret in plaintext **exactly once**. The **previous** secret keeps working for a **24-hour grace window** so you can roll out the new one with zero downtime. ```bash curl -X POST \ https://api.factuarea.com/v1/account/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/rotate_secret \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ```json { "data": { "object": "api_key", "id": "0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f", "prefix": "fact_live_Zq7mP4xV", "secret": "fact_live_Zq7mP4xVnR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "invoices:write"], "environment": "live" } } ``` <Callout type="warn"> During the 24-hour grace window both the new and the previous secret authenticate; a request still using the previous one receives a `199` `Warning` header counting down the hours left. Once the window expires the previous secret is rejected and purged. Roll the new secret out within those 24 hours. Rotation is irreversible. </Callout> ## Revoke a key [#revoke] `POST /v1/account/api-keys/{api_key}/revoke` invalidates a key permanently. Subsequent requests authenticated with it fail with `401`. An optional `reason` (max 500 chars) is recorded in the audit log. ```bash curl -X POST \ https://api.factuarea.com/v1/account/api-keys/0190f2b1-1c4e-7a3d-9f10-0a1b2c3d4e5f/revoke \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -d '{"reason": "Rotated out of the deploy pipeline"}' ``` <Callout type="warn"> Revocation is **irreversible** and is **not** restricted to other keys: you may revoke the key you are authenticating the request with, cutting off your own access. Make sure another valid key is in place first if you still need API access. </Callout> After revocation, requests with that key return `401` with the **generic** `invalid_api_key` code — not a distinct "revoked" code: ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "La API key proporcionada no es válida.", "request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV" } } ``` This is deliberate **anti-enumeration**: the API never reveals whether a key was revoked, expired or never existed — every unusable key looks the same to an attacker. Branch your own logic on the `200`/`401` outcome, not on a revoked-specific code. ## Scopes and isolation [#scopes-and-isolation] The five endpoints are gated by the `account` scopes: * `account:read` — list and retrieve keys. * `account:write` — create, rotate and revoke keys. All operations are scoped to the authenticated company. A key `id` belonging to another company returns `404 api_key_not_found` (again, anti-enumeration — it never reveals the key exists), never `403`. <Callout type="info"> Managing keys still requires an existing key with the right scopes. Create your **first** key in the developer dashboard ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)), then use these endpoints to provision the rest programmatically. See [Authentication](/guides/authentication) for the key format and headers. </Callout> --- # Authentication (/guides/authentication) The Factuarea API authenticates every request with an **API key**. Keys are opaque tokens generated in the developer dashboard ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)) and bound to a specific company. Every request to `https://api.factuarea.com/v1/*` must include a valid key in one of the two supported formats. ## API key format [#api-key-format] ``` fact_live_<24 alphanumeric characters> fact_test_<24 alphanumeric characters> ``` Example: ``` fact_live_8KqW3pXnR2VbY7TcA9eFmN5z fact_test_3pXnR2VbY7TcA9eFmN5z8KqW ``` * **Prefix**: determines the **environment**. `fact_live_` operates on your real company (production); `fact_test_` operates on an isolated sandbox company with external effects (VeriFactu → AEAT, FACe, emails, webhooks) switched off. The prefix lets you identify the environment without decoding the key. See [Test mode & sandbox](/guides/test-mode). * **Secret**: 24 base62 characters → \~143 bits of entropy. Shown **only once** at creation in the dashboard. If you lose it, you must rotate. * **DB hash**: the backend stores only the bcrypt cost-12 hash of the secret. There's no way to recover it. <Callout type="info"> Every example in this guide uses a `fact_live_` key, but the exact same request works with a `fact_test_` key — just swap the prefix to operate on sandbox data. Build and validate your integration in test first. See [Test mode & sandbox](/guides/test-mode). </Callout> ## Sending the key on each request [#sending-the-key-on-each-request] The API accepts two equivalent formats. Pick the one that fits your client: ### Authorization Bearer (recommended) [#authorization-bearer-recommended] ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` ### X-API-Key header [#x-api-key-header] ```bash curl https://api.factuarea.com/v1/clients \ -H "X-API-Key: fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Send only one of the two headers. If both are present, the `Authorization: Bearer` header takes precedence. ## Examples per language [#examples-per-language] <Tabs items="['PHP (Guzzle)', 'Node.js (fetch)', 'Python (requests)']"> <Tab value="PHP (Guzzle)"> ```php $client = new GuzzleHttp\Client([ 'base_uri' => 'https://api.factuarea.com/v1/', 'headers' => [ 'Authorization' => 'Bearer ' . getenv('FACTUAREA_API_KEY'), 'Accept' => 'application/json', ], ]); $response = $client->get('clients?limit=10'); $body = json_decode((string) $response->getBody(), true); ``` </Tab> <Tab value="Node.js (fetch)"> ```javascript const res = await fetch('https://api.factuarea.com/v1/clients?limit=10', { headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, Accept: 'application/json', }, }); const data = await res.json(); ``` </Tab> <Tab value="Python (requests)"> ```python import os import requests resp = requests.get( 'https://api.factuarea.com/v1/clients', params={'limit': 10}, headers={ 'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}", 'Accept': 'application/json', }, ) resp.raise_for_status() data = resp.json() ``` </Tab> </Tabs> ## OAuth 2.1 [#oauth] For agent integrations and third-party apps that act on behalf of a Factuarea user, the API also supports the **OAuth 2.1 authorization-code flow with PKCE** (`code_challenge_method=S256`) as an alternative to a static API key. The same [scopes](#scopes) gate the access token, and the [rotation policy](#rotation-policy) applies to OAuth client secrets too. Discovery metadata (RFC 8414) is published at `/.well-known/oauth-authorization-server`, so OAuth clients can resolve the authorization and token endpoints automatically: ```bash curl https://api.factuarea.com/.well-known/oauth-authorization-server ``` The `OAuth2` security scheme — including the authorization and token URLs and the full scope list — is described in the [API Reference](/api-reference). ## Scopes [#scopes] Each API key is created with one or more **scopes** that limit which endpoints it can invoke. Scopes are strings of the form `<resource>:<action>`. The catalog is **closed**: any scope outside the listed set raises `invalid_scope` when creating the key. ### Clients and catalog [#clients-and-catalog] | Scope | Allows | | ------------------ | ---------------------------- | | `clients:read` | List and retrieve clients. | | `clients:write` | Create and update clients. | | `clients:delete` | Delete clients. | | `products:read` | List and retrieve products. | | `products:write` | Create and update products. | | `products:delete` | Delete products. | | `suppliers:read` | List and retrieve suppliers. | | `suppliers:write` | Create and update suppliers. | | `suppliers:delete` | Delete suppliers. | ### Sales documents [#sales-documents] | Scope | Allows | | ---------------------------- | --------------------------------------------------------------- | | `invoices:read` | List and retrieve invoices. | | `invoices:write` | Create and update invoices (includes duplicate and corrective). | | `invoices:delete` | Delete invoice drafts. | | `invoices:send` | Send invoice by email to the client. | | `invoices:void` | Void an issued invoice. | | `quotes:read` | List and retrieve quotes. | | `quotes:write` | Create and update quotes. | | `quotes:delete` | Delete quotes. | | `quotes:send` | Send quote by email. | | `quotes:transition` | Accept, reject or convert quotes. | | `proformas:read` | List and retrieve pro-forma invoices. | | `proformas:write` | Create and update pro-forma invoices. | | `proformas:delete` | Delete pro-forma invoices. | | `proformas:send` | Send pro-forma invoice by email. | | `proformas:transition` | Convert pro-forma invoice to invoice. | | `delivery_notes:read` | List and retrieve delivery notes. | | `delivery_notes:write` | Create and update delivery notes. | | `delivery_notes:delete` | Delete delivery notes. | | `delivery_notes:transition` | Mark as delivered/cancelled, sign, convert. | | `delivery_notes:gdpr_forget` | Erase signature-audit PII (GDPR Art. 17). | ### Purchases and recurring [#purchases-and-recurring] | Scope | Allows | | ------------------------------- | -------------------------------------- | | `purchase_invoices:read` | List and retrieve vendor bills. | | `purchase_invoices:write` | Create and update vendor bills. | | `purchase_invoices:delete` | Delete vendor bills. | | `purchase_invoices:transition` | Mark as paid, received, accounted. | | `recurring_invoices:read` | List and retrieve recurring templates. | | `recurring_invoices:write` | Create and update recurring templates. | | `recurring_invoices:delete` | Delete recurring templates. | | `recurring_invoices:transition` | Pause, resume and emit manually. | ### Catalogs and export [#catalogs-and-export] | Scope | Allows | | ------------------- | ---------------------------------------------------------------------------------------------- | | `taxes:read` | Read the (global) tax rates catalog. | | `taxes:write` | Create and update tax rates. | | `taxes:delete` | Delete tax rates. | | `series:read` | List invoice numbering series. | | `series:write` | Create and update invoice numbering series. | | `pdfs:read` | Download PDFs of any document with the matching `:read` scope. | | `tax_reports:read` | Read tax reports (Modelo 303/347, etc.). | | `tax_reports:write` | Generate tax reports. | | `account:read` | Read the authenticated account (`GET /v1/account`). | | `account:write` | Manage the account's own API keys (create, rotate, revoke) and update account personalization. | ### VeriFactu & FacturaE [#verifactu--facturae] | Scope | Allows | | ----------------- | ---------------------------------------------------------------------- | | `verifactu:read` | Read VeriFactu records, events, certificates and config. | | `verifactu:write` | Manage VeriFactu certificates, settings and retries. | | `facturae:read` | Download the FacturaE XML of an invoice and read its FACe submissions. | | `facturae:write` | Submit invoices to FACe and request submission cancellations. | ### Webhooks and events [#webhooks-and-events] | Scope | Allows | | ----------------- | -------------------------------------------------- | | `webhooks:read` | List webhook endpoints and deliveries. | | `webhooks:write` | Create, update, rotate and ping webhook endpoints. | | `webhooks:delete` | Delete webhook endpoints. | | `events:read` | Read the event catalog and individual events. | ### Managed companies (gestoría) [#managed-companies-gestoría] Fine-grained scopes for the **gestoría** model, where a master account manages child sub-companies and their API keys. Reachable only with an API key (no OAuth consent equivalent); `companies:*` also requires the gestoría plan module. | Scope | Allows | | ------------------ | ---------------------------------------------------------- | | `companies:read` | List and retrieve managed companies (child sub-accounts). | | `companies:write` | Create, update, activate and deactivate managed companies. | | `companies:delete` | Archive managed companies. | | `api_keys:read` | List and retrieve API keys of managed companies. | | `api_keys:write` | Create, rotate and revoke API keys of managed companies. | | `api_keys:delete` | Permanently delete API keys of managed companies. | ### Super-scope [#super-scope] | Scope | Allows | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `*` | Full access — equivalent to having every other scope above. Reserved for owner keys / one-off migrations. **Avoid using in production integrations**. | If a request uses an endpoint that requires a scope not granted to the key, the response is `403` with `type: authorization_error` and `code: insufficient_scope`. ```json { "error": { "type": "authorization_error", "code": "insufficient_scope", "message": "La API key no tiene el scope requerido para esta operación.", "request_id": "req_01JBVH7..." } } ``` ## Key management [#key-management] API keys are managed from the developer dashboard ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)), not via the public API. From there you can create keys, rotate their secret, revoke them, configure scopes, an optional `expires_at`, and an IP allowlist. The authenticated key's metadata (id, name, prefix, scopes, tier, `last_used_at`, `expires_at`) is readable via `GET /v1/account` — but the secret is **never** returned. <Callout type="warn"> **There is no endpoint to "view" the secret**. It is shown only once at creation. If you lose the value you must rotate the key in the dashboard and redeploy the new secret. This is deliberate: it minimizes the exposure window. </Callout> ### Rotation policy [#rotation-policy] API keys and OAuth client secrets are long-lived credentials and must be rotated on a schedule and immediately after any suspected leak. * **Prefixes** are the source of truth for the environment: `fact_live_` (production) and `fact_test_` (sandbox). Never mix them across environments. * **Rotate** from the dashboard (or via the self-service [`account:write` endpoints](/guides/api-keys#rotate)) to issue a brand-new secret. The new secret is returned **once** — store it immediately, it is never shown again. * **Grace window (dual-secret).** After a rotation the previous secret keeps working for a **24-hour grace window**, so you can roll out the new secret with zero downtime. During that window both the new and the previous secret are accepted; once the window expires the previous secret is rejected and purged. A request still using the previous secret receives a `199` `Warning` header telling you how many hours remain before it stops working. * **When to rotate**: on a regular schedule (e.g. every 90 days), whenever a teammate with access leaves, and **immediately** if a secret is ever exposed in logs, source control or a public client. * **Revoke** to invalidate a key permanently. Any subsequent request with it fails with `401`. Revocation has no grace window — it is instant and irreversible. Secrets are bound to a single company (tenant) and must never be embedded in browsers, mobile apps or any public client — keep them server-side only. ### IP allowlist [#ip-allowlist] Each API key can be restricted to a list of IPs or CIDR ranges from the dashboard. If the request comes from an IP outside the allowlist, the response is `401` and the incident is recorded in the audit log. Leave the allowlist empty to allow any IP. ## Authentication errors [#authentication-errors] Failures related to the API key respond with HTTP `401` (or `403` for `insufficient_scope`) and the standard error envelope. The `code` field distinguishes the case: | `code` | HTTP | Cause | | ------------------------ | ----- | ------------------------------------------------------------------------------------------ | | `missing_api_key` | `401` | No authentication header sent. | | `invalid_api_key` | `401` | The key does not exist, has the wrong format, or the secret doesn't match the stored hash. | | `api_key_revoked` | `401` | The key was revoked or has expired. Create a new one in the dashboard. | | `too_many_auth_failures` | `429` | Too many failed authentication attempts; back off. | | `insufficient_scope` | `403` | The key lacks the scope the endpoint requires. | Every response includes a unique `request_id` (also in the `X-Request-Id` header) you can pass to support when investigating. ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "La API key proporcionada no es válida.", "request_id": "req_01JBVH7K9Y4N3CDQ2EHJB1AGSV", "doc_url": "https://docs.factuarea.com/guides/errors#invalid_api_key" } } ``` ## Best practices [#best-practices] * **Never** commit API keys to repositories — use environment variables or a secret manager (AWS Secrets Manager, Doppler, 1Password Service Accounts). * Create **one key per integration**: makes rotating and auditing access easier without affecting the rest. * Limit scopes to the minimum required. An export script only needs specific `:read` scopes. * Enable IP allowlist for server-to-server integrations with stable IPs. * Configure `expires_at` for temporary keys (e.g. consultancies, demos). * Audit usage from the dashboard: `Developers > API Keys > Activity` shows IPs, paths and errors per key. --- # Bulk operations (/guides/bulk-operations) Bulk endpoints process several rows in a single request and **never fail the whole batch because one row was rejected**. Each row is evaluated independently and the response reports, per row, whether it was applied or not. This is the **partial-success** contract, shared by every bulk endpoint of the public API. The bulk surface now spans **delete, create, pdf, send and status** operations across the document and catalog resources — not just the original `bulk-delete` family. Some return the `BulkPartialSuccessResult` shape below, `bulk-create` returns the richer `BulkCreateResult`, and `bulk-pdf` streams a binary ZIP instead of the JSON envelope. Every one of them honours partial-success: one bad row never sinks the batch. ## Response shape [#response-shape] A bulk operation always returns `200 OK` with a `BulkPartialSuccessResult` inside `data`: ```json { "data": { "total": 3, "successful": 2, "failed": 1, "failures": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "error_code": "resource_not_deletable", "error_message": "La factura ya está emitida y no se puede eliminar." } ] } } ``` | Field | Type | Meaning | | ------------ | ------- | ---------------------------------------------------------------------------------- | | `total` | integer | Rows processed (`successful + failed`). | | `successful` | integer | Rows applied (deleted, created or validated). | | `failed` | integer | Rows that could not be processed. Equals `failures` length. | | `failures` | array | One item per failed row. Always a list — empty, never `null`, when nothing failed. | The invariant `total === successful + failed` and `failed === failures.length` always holds. A fully successful batch returns `failures: []`. ## A failure item [#a-failure-item] Each entry in `failures` identifies the row and explains why it was rejected. Identity is **polymorphic**: * `id` — the UUID of an existing resource (bulk-delete and other operations over existing resources). * `index` — the 0-based position of the row in the batch, for new rows that do not have a resource yet (bulk-create with `dry_run`, CSV import). Exactly one of `id` / `index` is present. | Field | Type | Meaning | | --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- | | `id` | string | UUID v7 of the existing resource that could not be processed. | | `index` | integer | 0-based position of the row within the batch. | | `error_code` | string | Machine-readable code from the v1 error catalog (stable across languages). Branch on this. | | `error_message` | string | Human-readable reason, in Spanish. For display, not for branching. | | `errors` | array | Per-field blocking issues (`FieldIssue[]`). Present in `validate-only` / `bulk-create` flows; absent for `bulk-delete`. | | `warnings` | array | Per-field non-blocking warnings (`FieldIssue[]`). | <Callout type="info"> Branch on **`error_code`**, never on `error_message` — the message is Spanish, human-facing text and may change. For bulk-delete the codes are `resource_not_found` (the UUID does not exist or belongs to another company) and `resource_not_deletable` (the resource exists but its state forbids deletion: a signed delivery note, an invoiced quote, a client with documents, etc.). </Callout> ## Reading the result [#reading-the-result] Don't treat the call as all-or-nothing. Inspect `failures` and act per row: <Tabs items="['cURL', 'TypeScript', 'Python']"> <Tab value="cURL"> ```bash curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-delete \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02", "01931b3e-...a03"] }' \ | jq '.data | {total, successful, failed, failures}' ``` </Tab> <Tab value="TypeScript"> ```ts const { data } = await factuarea.quotes.bulkDelete({ ids }); if (data.failed > 0) { for (const f of data.failures) { // f.id, f.error_code, f.error_message console.warn(`${f.id} → ${f.error_code}: ${f.error_message}`); } } ``` </Tab> <Tab value="Python"> ```python res = factuarea.quotes.bulk_delete(ids=ids) data = res["data"] for f in data["failures"]: # branch on error_code, show error_message print(f["id"], f["error_code"], f["error_message"]) ``` </Tab> </Tabs> ## Foreign and unknown UUIDs [#foreign-and-unknown-uuids] UUIDs that do not belong to your company, or that do not exist, are **never a global 404**. The handler filters by `company_id`, so a foreign or unknown UUID is reported as a normal failure (`resource_not_found`) — it never leaks whether a resource exists in another tenant. ## Bulk create (validate-only with dry\_run) [#bulk-create-validate-only-with-dry_run] `bulk-create` accepts up to **100** rows for invoices and up to **500** for clients in a single call, and returns a richer envelope, `BulkCreateResult`. The `dry_run` flag (default `false`) switches between two behaviours: * **`dry_run: true`** validates each row **without persisting anything** and returns a per-row `results[]`. Every entry carries its `index`, a `status`, and the `errors[]` / `warnings[]` found for that row. Nothing is written — use it to surface problems in your UI before you commit. * **`dry_run: false`** creates **only the valid rows**. Rows that fail validation are not created and come back in `failures[]`, each identified by its 0-based `index`. The envelope carries both arrays so the same parser works in either mode: ```json { "data": { "dry_run": true, "total": 2, "successful": 1, "failed": 1, "results": [ { "index": 0, "status": "valid", "errors": [], "warnings": [] }, { "index": 1, "status": "invalid", "errors": [{ "field": "client_id", "code": "required", "message": "El cliente es obligatorio." }], "warnings": [] } ], "failures": [ { "index": 1, "error_code": "validation_failed", "error_message": "Faltan campos obligatorios en la fila." } ] } } ``` Validate first with `dry_run: true`, fix what `results[]` flags, then resend the same payload with `dry_run: false` to persist the rows that pass: ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-create \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9f1c2b7a-0e44-4c1a-8f3d-1a2b3c4d5e6f" \ -d '{ "dry_run": true, "invoices": [ { "client_id": "01931b3e-...c01", "lines": [{ "description": "Consulting", "quantity": 1, "unit_price": "100.00", "tax_rate_id": "01931b3e-...t21" }] }, { "lines": [{ "description": "Missing client", "quantity": 1, "unit_price": "50.00" }] } ] }' | jq '.data | {dry_run, total, successful, failed, results, failures}' ``` ## Bulk PDF (ZIP download) [#bulk-pdf-zip-download] `bulk-pdf` packages the PDFs of up to **50** documents into a single ZIP and streams the **binary archive** back — it does **not** return the JSON envelope. Ids that are not found, or that have no PDF available, do **not** abort the request: the ZIP carries only the valid documents, and the per-id counts travel in `X-Bulk-*` response headers so you can reconcile what made it in. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-pdf \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"] }' \ -D - -o invoices.zip ``` The `-D -` flag dumps the response headers: read `X-Bulk-Total`, `X-Bulk-Successful` and `X-Bulk-Failed` to know how many ids made it into the archive. ## Bulk send [#bulk-send] `bulk-send` queues up to **200** documents to be emailed and returns the `BulkPartialSuccessResult` shape. Delivery is asynchronous: a `successful` row means the email was **queued**, not yet delivered. The optional `to`, `cc`, `subject`, `message` and `language` fields override the defaults for the whole batch. ```bash curl -s -X POST https://api.factuarea.com/v1/quotes/bulk-send \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c3e1f90-2a11-4b22-9d44-5e6f7a8b9c0d" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "language": "es" }' \ | jq '.data | {total, successful, failed, failures}' ``` ## Bulk status transitions [#bulk-status-transitions] `bulk-status` moves up to **50** documents to a new status, each transition going **through the Aggregate guard** — a row whose current state forbids the move fails individually and lands in `failures[]`, while the rest still transition. The target `new_status` must belong to the closed set allowed for that resource (see the table below). For invoices, `payment_date` is **required** when `new_status` is `paid`. For purchase invoices, `payment_date` is likewise required and propagated **as given** — it is never silently replaced with `now()`. For products and suppliers the transition is **idempotent**: a resource already in the requested state counts as `successful` without flipping anything. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/bulk-status \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 3b8d2e10-4f55-4c66-8a77-9b0c1d2e3f40" \ -d '{ "ids": ["01931b3e-...a01", "01931b3e-...a02"], "new_status": "paid", "payment_date": "2026-03-20" }' \ | jq '.data | {total, successful, failed, failures}' ``` The allowed `new_status` values per resource: | Resource | Allowed `new_status` | | ------------------- | --------------------------------- | | `invoices` | `sent`, `paid` | | `quotes` | `approved`, `rejected` | | `proformas` | `accepted`, `rejected` | | `delivery_notes` | `delivered`, `cancelled` | | `purchase_invoices` | `paid` | | `products` | `active`, `inactive` (idempotent) | | `suppliers` | `active`, `inactive` (idempotent) | ## Endpoints and limits [#endpoints-and-limits] Each operation caps the batch at a fixed number of rows. Splitting a larger job into chunks within these limits is up to you: | Operation | Resources | Max rows | Response shape | | ------------- | ------------------------------------------------------------------------------------------------- | --------- | -------------------------- | | `bulk-create` | `invoices` (100), `clients` (500) | 100 / 500 | `BulkCreateResult` | | `bulk-pdf` | `invoices`, `quotes`, `proformas`, `delivery_notes` | 50 | binary ZIP + `X-Bulk-*` | | `bulk-send` | `invoices`, `quotes`, `proformas`, `delivery_notes` | 200 | `BulkPartialSuccessResult` | | `bulk-status` | `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices`, `products`, `suppliers` | 50 | `BulkPartialSuccessResult` | | `bulk-delete` | all nine resources | — | `BulkPartialSuccessResult` | <Callout type="info"> **Use `Idempotency-Key` on the mutating bulk ops.** `bulk-create`, `bulk-send`, `bulk-status` and `bulk-delete` all accept the `Idempotency-Key` header, so a retry after a dropped connection replays the original result instead of running the batch twice. `bulk-pdf` is a pure read and needs no key. </Callout> ## Versioning — the legacy shape [#versioning--the-legacy-shape] The partial-success shape is the current contract. Integrators **anchored to a version before `2026-09-01`** (via the `Factuarea-Version` header or a pin on the API key) keep receiving the previous bulk-delete shape, so no existing integration breaks: ```json { "object": "bulk_delete_result", "deleted": 2, "failed": [ { "id": "01931b3e-...a01", "reason": "La factura ya está emitida y no se puede eliminar." } ] } ``` The mapping between the two shapes is mechanical: `deleted` is the new `successful`, and each legacy `failed[].reason` is the new `failures[].error_message` (the new shape adds the stable `error_code` and the `total` counter on top). Send no header — or a date on or after `2026-09-01` — to get the partial-success shape. <Callout type="warn"> Pin a version only to freeze a contract you already depend on. New integrations should use the partial-success shape: it carries a stable, language-independent `error_code` you can branch on, which the legacy `reason` string does not. </Callout> --- # AEAT census verification (/guides/census-verification) Factuarea can check that your company's **registered name + NIF pair** is correctly identified in the **AEAT census** — the same identification the AEAT performs when it receives your VeriFactu invoice records. A pair that is not censused gets the submission rejected (AEAT error 4104, *holder not identified*), so verifying **early** — right after registration and whenever your fiscal data changes — saves you rejected submissions later. ``` POST /v1/account/census-verification ``` * **Scope:** `account:read` * **Request body:** none — the check always runs against the **persisted** name and `tax_id` of the authenticated account. It never accepts an arbitrary NIF or name in the payload. * **Side effect:** the result is stored as a snapshot on your company (visible in the Factuarea app settings). Changing your company name or `tax_id` resets the snapshot until you verify again. <Callout type="info"> This is the verification Factuarea also offers in the app during onboarding. Negative results are **informational**: they never block registration, invoicing or any other operation — they just warn you that VeriFactu submissions may be rejected until the census data is fixed. </Callout> ## Calling the endpoint [#calling-the-endpoint] ```bash curl -X POST https://api.factuarea.com/v1/account/census-verification \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` Response (`200`): ```json { "data": { "object": "census_verification", "status": "identified", "verified_name": "ACME SOLUTIONS SL", "checked_at": "2026-06-10T22:15:04+00:00" } } ``` `verified_name` is the company name that was contrasted against the census (the persisted one). `checked_at` is the moment of the verification, ISO 8601\. With the official SDKs: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); const result = await factuarea.account.verifyCensus(); // result.data.status → "identified" | "not_identified" | … ``` </Tab> <Tab value="PHP"> ```php <?php use Factuarea\Sdk\Custom\FactuareaClient; $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); $response = $factuarea->account->publicApiV1AccountVerifyCensus(); ``` </Tab> </Tabs> ## Verify your clients too [#clients] The AEAT runs the same identification on the **recipient** of every VeriFactu invoice record: a client whose name + NIF pair is not censused gets the submission rejected with AEAT error **1239** (*recipient not identified*). Factuarea **deliberately does not validate clients against the census when you create them** — and that is by design, not an oversight: foreign clients have no Spanish census entry, newly incorporated companies may take days to appear, simplified B2C invoices carry no recipient NIF at all, and the AEAT service itself can be down (the whole feature is fail-open). Blocking client creation on the census would break all those legitimate flows. The recommended pattern instead: **verify the name + NIF pair right before issuing VeriFactu invoices to that client** with the dedicated endpoint: ``` POST /v1/clients/census-verification ``` * **Scope:** `clients:read` * **Request body:** `tax_id` (NIF/CIF/NIE) and `name` — the pair is checked **together**, exactly as the AEAT will check it on submission. It doesn't need to match an existing client: the check is **stateless** and persists nothing on your clients. * **Rate limit:** 5 verifications per minute, like the account endpoint. ```bash curl -X POST https://api.factuarea.com/v1/clients/census-verification \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"tax_id": "B12345674", "name": "CONSTRUCCIONES PEREZ SL"}' ``` Response (`200`): ```json { "data": { "object": "census_verification", "status": "identified", "verified_name": "CONSTRUCCIONES PEREZ SL", "checked_at": "2026-06-11T09:30:00Z" } } ``` The `status` values are the same six as the account verification (table below). How to act on them for a client: * `identified` — issue normally. * `not_identified` — a VeriFactu submission to this recipient is **guaranteed to be rejected with 1239**. Ask the client for their exact registered name and NIF before issuing. * `not_identified_similar` — (individuals) use the exact name as registered with the AEAT. * `unavailable` — the AEAT could not answer; the check is informational, so you may issue anyway and retry the verification later. <Callout type="info"> If a rejected submission slips through anyway, all is not lost: [subsanación](/guides/verifactu-subsanacion) lets you fix the data and resubmit the same record. Census verification up front plus subsanación as the safety net covers the whole 1239 lifecycle. </Callout> ## Possible states [#possible-states] `status` is always one of these six values: | `status` | Meaning | What to do | | ------------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `identified` | The name + NIF pair matches the census. | Nothing — you are ready for VeriFactu. | | `not_identified` | The NIF is not identified in the census with that name. | Review your fiscal data: exact registered name and NIF. Submissions risk rejection. | | `not_identified_similar` | (Individuals only.) The NIF exists but the name only partially matches. | Use the exact name as registered with the AEAT. | | `identified_inactive` | The NIF is identified but registered as inactive (*baja*) in the census. | Check your census situation with the AEAT. | | `identified_revoked` | The NIF is identified but has been revoked. | Check your census situation with the AEAT. | | `unavailable` | The AEAT service could not be reached or returned an unrecognized response. | Retry later. This is **not** an error. | Branch on `status`, the values are a frozen contract. Note that **negative states are not HTTP errors**: every completed verification returns `200`. ## Fail-open by design [#fail-open-by-design] The verification never breaks your flow because the AEAT is down: * AEAT timeout, SOAP fault or unrecognized response → `200` with `status: unavailable`. Never a `5xx` for this cause. * Results are cached server-side for a short period, so immediate retries of the same pair don't hit the AEAT again (`unavailable` is cached for only a few seconds so you can retry soon). ## Errors [#errors] The only business error is a company without fiscal data: ```json { "error": { "type": "invalid_request_error", "code": "census_requires_tax_id", "message": "Configura primero los datos fiscales de tu empresa para verificar el censo.", "param": "tax_id" } } ``` | HTTP | `code` | When | | ---- | ------------------------------------- | ------------------------------------------- | | 401 | `missing_api_key` / `invalid_api_key` | Missing or invalid API key. | | 403 | `insufficient_scope` | The key lacks the `account:read` scope. | | 422 | `census_requires_tax_id` | The account has no `tax_id` configured yet. | | 429 | `rate_limit_exceeded` | More than **5 verifications per minute**. | See the [error envelope guide](/guides/errors) for the full error contract. ## Rate limit [#rate-limit] The endpoint is limited to **5 verifications per minute** per account, independently of your key's global rate-limit tier. Above that you get a `429` — wait for the window to reset and retry. ## Test mode: magic NIFs [#test-mode-magic-nifs] With a `fact_test_` key ([test mode](/guides/test-mode)) the verification **never reaches the AEAT**. The sandbox returns deterministic states based on the sandbox company's `tax_id`, so you can exercise every branch of your integration: | Sandbox `tax_id` | Returned `status` | | ---------------- | ------------------------ | | `00000000T` | `identified` | | `11111111H` | `not_identified` | | `22222222J` | `not_identified_similar` | | `33333333P` | `identified_inactive` | | `44444444A` | `identified_revoked` | | `55555555K` | `unavailable` | | any other NIF | `identified` (default) | All magic NIFs carry a valid control letter, so they pass standard NIF validation. Set the sandbox company's `tax_id` to the magic value you want to test, then call the endpoint with your `fact_test_` key: ```bash curl -X POST https://api.factuarea.com/v1/account/census-verification \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` ```json { "data": { "object": "census_verification", "status": "identified_revoked", "verified_name": "SANDBOX COMPANY SL", "checked_at": "2026-06-10T22:15:04+00:00" } } ``` <Callout type="warn"> In live mode the real AEAT census is consulted with Factuarea's platform certificate. The states reflect the AEAT response verbatim — Factuarea never invents a state. </Callout> --- # Child company API keys (/guides/child-api-keys) Each [managed company](/guides/companies) has its own set of API keys. A child key authenticates requests **on behalf of that one company** — it never reaches sibling companies or the master tenant. It is the alternative to driving a child with the [`X-Active-Profile` header](/guides/acting-on-behalf): a child key is bound to one company for good, rather than switched per request. Five endpoints under `/v1/companies/{id}/api-keys` cover the lifecycle. | Operation | Endpoint | Scope | | -------------------- | ------------------------------------------------------ | ---------------- | | List child keys | `GET /v1/companies/{id}/api-keys` | `api_keys:read` | | Create a child key | `POST /v1/companies/{id}/api-keys` | `api_keys:write` | | Retrieve a child key | `GET /v1/companies/{id}/api-keys/{key}` | `api_keys:read` | | Rotate the secret | `POST /v1/companies/{id}/api-keys/{key}/rotate-secret` | `api_keys:write` | | Revoke a child key | `DELETE /v1/companies/{id}/api-keys/{key}` | `api_keys:write` | This mirrors the account-level [API keys](/guides/api-keys) self-service, scoped to a child company instead of your own account. Both `{id}` (the company) and `{key}` (the API key) are opaque UUID v7 values. ## Create a child key [#create] `POST /v1/companies/{id}/api-keys` mints a key for the company and returns its plaintext `secret` **exactly once**. Requires the `api_keys:write` scope. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Producción Talleres García", "scopes": ["invoices:read", "invoices:write"] }' ``` Response (`201`): ```json { "data": { "object": "api_key", "id": "0190f2c0-77aa-7b21-8c33-1d2e3f405162", "name": "Producción Talleres García", "prefix": "fact_live_1OSf9KdP", "secret": "fact_live_1OSf9KdPR2VbY7TcA9eFmN5z", "scopes": ["invoices:read", "invoices:write"], "tier": "scale", "environment": "live" } } ``` <Callout type="warn"> The `secret` is shown **only** in this `201` response and after a rotation. No endpoint returns it later. Persist it to a secret manager the moment you receive it — never to a log or a repository. If you lose it, rotate the key. </Callout> ### Scopes must be a subset of the parent key [#subset] The scopes you request for a child key **must be a subset of the scopes of the key making the call**. Requesting a scope the calling key does not hold returns `422` with per-field errors — there is **no silent narrowing**: the key is not created with a trimmed scope list, the whole request fails. ```json { "error": { "type": "validation_error", "code": "validation_failed", "message": "No puedes conceder un scope que tu propia key no tiene.", "param": "scopes" } } ``` So a key holding `invoices:read invoices:write` can mint child keys with any subset of those two scopes, but never `clients:write`. Provision a sufficiently scoped master key first, then derive narrower child keys from it. The `environment` and `tier` are never taken from the body — they are inherited from the calling key. | Field | Required | Notes | | -------------- | -------- | ----------------------------------------------------------------------------------------------- | | `name` | yes | Human-readable label (1–120 chars). | | `scopes` | yes | One or more [scopes](/guides/authentication#scopes), each a subset of the calling key's scopes. | | `expires_at` | no | Future ISO 8601 instant after which the key stops authenticating. | | `ip_allowlist` | no | Optional list of allowed IPs / CIDR ranges (IPv4, IPv6, `/N`). | ## Rotate the secret [#rotate] `POST /v1/companies/{id}/api-keys/{key}/rotate-secret` invalidates the current secret immediately, generates a fresh `prefix` + `secret`, and returns the new secret in plaintext **exactly once**. Requires the `api_keys:write` scope. ```bash curl -X POST \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys/0190f2c0-77aa-7b21-8c33-1d2e3f405162/rotate-secret \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` <Callout type="warn"> Rotation takes effect **right away**: any request still using the old secret stops authenticating the instant you rotate. Deploy the new secret before — or atomically with — the rotation to avoid downtime. This is irreversible. </Callout> ## Revoke a child key [#revoke] `DELETE /v1/companies/{id}/api-keys/{key}` revokes a child key permanently. Subsequent requests authenticated with it stop working. Requires the `api_keys:write` scope. ```bash curl -X DELETE \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/api-keys/0190f2c0-77aa-7b21-8c33-1d2e3f405162 \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` <Callout type="warn"> Revocation is **irreversible**. Once revoked, the key cannot be restored — mint a new one if the company still needs API access. </Callout> ## Scopes and isolation [#scopes] Child keys are gated by the `api_keys` scopes: * `api_keys:read` — list and retrieve child keys. * `api_keys:write` — create, rotate and revoke child keys. Every operation is scoped to your master tenant. A company `id` belonging to a different master returns `404` — never `403`, and never a child-key endpoint for a company you do not manage. This is the same cross-master isolation that governs [the companies themselves](/guides/companies#scopes): a master can only ever see and act on its own companies and their keys. --- # Managed companies (/guides/companies) 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](/guides/child-api-keys) scoped to it, or keep your master key and switch target company per request with the [`X-Active-Profile` header](/guides/acting-on-behalf). 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](/api-reference/companies/public-api.v1.companies.list). ## Create a managed company [#create] `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. ```bash 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`): ```json { "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 [#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](#lifecycle), a different axis from the [`provisioning_status`](#provisioning) 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`. <Callout type="info"> 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. </Callout> ### Duplicate tax id [#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: ```json { "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 [#provisioning] 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-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. | <Callout type="info"> 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. </Callout> ### Per-seat billing [#per-seat] 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](#seat-charge-preview). <Callout type="info"> 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. </Callout> ### Poll the creation status [#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. ```bash curl https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c/creation-status \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Response (`200`): ```json { "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 [#verify-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. ```bash 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 [#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](#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 [#deactivate] `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. ```bash 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 [#activate] `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. ```bash 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 [#activate-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. ```bash 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 [#seat-charge-preview] `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`. ```bash curl "https://api.factuarea.com/v1/companies/seat-charge-preview?count=1" \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` Response (`200`): ```json { "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 [#list] `GET /v1/companies` returns your managed companies with [cursor pagination](/guides/pagination); `GET /v1/companies/{id}` returns one. Both are scoped to your master tenant. ```bash 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 [#update] `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. ```bash 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 [#archive] `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. ```bash curl -X DELETE \ https://api.factuarea.com/v1/companies/01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c \ -H "Authorization: Bearer fact_live_8KqW3pXnR2VbY7TcA9eFmN5z" ``` <Callout type="warn"> 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. </Callout> ## Scopes and isolation [#scopes] 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](/guides/acting-on-behalf) and its [API keys](/guides/child-api-keys). --- # Corrective invoices (/guides/corrective-invoices) A corrective invoice (*rectificativa*) is a fiscal document in its own right: it gets its own number, its own registration at the AEAT and its own effect on the VAT return. Issuing one involves four independent decisions, and integrations tend to conflate them: 1. **Which invoice** may be corrected. 2. **Which R-code** the correction carries — the legal reason. 3. **Substitution or differences** — whether the corrective states the correct amounts or only the delta. 4. **Which lines** the corrective ends up with. Get the third and fourth wrong together and you will file a VAT return with the sign inverted. ## When this applies [#when] The original invoice must be `sent` or `paid`. Nothing else qualifies ([`BR-INV-001`](#traceability), RD 1619/2012 art. 15): | Original status | Corrective? | | -------------------- | -------------------------------------------------------- | | `sent`, `paid` | Yes. | | `draft`, `cancelled` | No — edit or delete it, it is not a fiscal document yet. | | `overdue` | No. Register the payment or annul it first. | | `annulled` | No — it has already been retired. | | Already a corrective | No. Issue a new corrective **of the original invoice**. | For a paid invoice this is not one option among several: it is the only one. A paid invoice cannot be annulled, because its output VAT is already committed to a period ([`BR-INV-023`](#traceability)). See [Annul or correct](/guides/annul-vs-correct). ## The R-code matrix is legal, not cosmetic [#r-codes] The R-code states *why* the original is being corrected, and the AEAT constrains which codes are legal for which original. | Original invoice type | Legal codes | | ------------------------------ | ------------------ | | Simplified `F2` | **`R5` only** | | Complete `F1`, substitute `F3` | **`R1`–`R4` only** | Force a code outside its row and the API answers `422` with the legal values in `allowed_values` ([`BR-INV-035`](#traceability)). There are two ways to arrive at the code. By default it is **derived** from the `correction_reason` slug you send ([`BR-INV-018`](#traceability)): | `correction_reason` | Code | Legal basis | | -------------------------------------------------------------------- | ---- | -------------------------------------------------------- | | `error_fundado` | `R1` | Art. 80.Uno, Dos y Seis LIVA — well-founded error in law | | `concurso` | `R2` | Art. 80.Tres LIVA — insolvency proceedings | | `incobrable` | `R3` | Art. 80.Cuatro LIVA — bad debt | | `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras` | `R4` | RD 1619/2012 art. 15 — remaining causes | `R5` is never derived from a reason. It comes from the *type* of the original: a corrective of an `F2` is always born `R5`, whatever reason you pass ([`BR-INV-019`](#traceability)). Alternatively you set `correction_code` explicitly. On a complete original, an explicit `R1`–`R4` **wins over** the slug-based derivation and becomes the code that travels in the VeriFactu chain. Use it when your own system already knows the legal cause and you do not want it inferred from a slug. `R2` and `R3` require supporting documentation by law. Pass `justification` (10 to 1000 characters); it is prepended to the corrective's notes as documentary traceability. ## Substitution or differences [#nature] This is the decision with the largest blast radius, and in the v1 contract you do not set it directly — you set `correction_type` and the nature follows: | `correction_type` | Nature | The corrective contains | Sign | | ----------------- | ------------------ | ---------------------------------------------------------------------- | ------------------------ | | `full` | `S` — substitution | The **correct amounts, complete**. It replaces the original in full. | Always positive or zero. | | `partial` | `I` — differences | Only the **difference** between what was invoiced and what is correct. | May be negative. | The rule the tax engine enforces: a taxable base may be negative **only** in a corrective by differences. In a substitution — and in any ordinary invoice — a negative base is incoherent data and is rejected with `422` ([`BR-VFC-033`](#traceability), [`BR-INV-017`](#traceability)). This is the mechanism for a downward correction. A refund is a corrective by differences whose base and VAT quota are negative, and the AEAT accepts it precisely because that is the fiscally correct way to express a credit. Trying to express the same refund as a substitution with negative amounts is rejected. <Callout type="warn"> A corrective by differences on an invoice at 0% VAT has a **negative base and a zero quota** — not a negative quota. The tax engine inherits the rate of the original lines and never invents one; a fabricated 21% here is the classic way to get a rejection from the AEAT. </Callout> ## What the API sends [#api] [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective), scope `invoices:write`. It answers `201` with the **new** invoice and a `Location` header pointing at it. | Field | Required | Notes | | ------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `correction_reason` | Yes | One of the eight slugs above. | | `correction_type` | Yes | `full` or `partial`. | | `correction_code` | No | `R1`–`R5`. Validated against the legal matrix. | | `justification` | No | 10–1000 characters. Mandatory in practice for `R2` and `R3`. | | `notes` | No | Free text, up to 1000 characters. | | `lines` | Required when `correction_type` is `partial` | `description`, `quantity`, `unit_price`, and optionally `tax_rate`, `discount_percent`, `indirect_tax_regime`, `product_id`. | The API Reference ships one ready-to-send example per code — `r1_error_fundado`, `r2_concurso`, `r3_incobrable`, `r4_otras` and `r5_simplificada` — in the request-body examples dropdown of that operation. They are also published as reusable `components.examples.corrective_*` entries in the OpenAPI document, so generated clients can resolve them by `$ref`. ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/corrective \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "correction_reason": "otras", "correction_type": "partial", "correction_code": "R3", "justification": "Crédito declarado incobrable por resolución judicial firme.", "lines": [ { "description": "Ajuste por impago", "quantity": -1, "unit_price": 100, "tax_rate": 21 } ] }' ``` The response is an ordinary invoice object whose `is_corrective` is `true` and whose `corrective` block carries `original_id`, `original_number`, `original_date`, `correction_reason`, `correction_type`, `correction_nature`, `base_rectificada`, `cuota_rectificada` and `correction_aeat_type` — the last one being the R-code that actually travelled to the AEAT. To list every corrective issued against an invoice, use [`GET /v1/invoices/{id}/correctives`](/api-reference/invoices/public-api.v1.invoices.correctives). ### How the lines are built [#lines] The three combinations produce genuinely different documents ([`BR-INV-036`](#traceability)): **`full` with no `lines`** — a full cancellation. One line is generated per original line with the quantity **negated**, preserving the product, the price, the tax rate, the retention, the surcharge, the discount and the indirect-tax regime of the original. **`full` with `lines`** — a substitution. The lines you send **are** the final lines; there is no diffing. Every field you omit is inherited **by index** from the equivalent original line, taxation included. The inheritance never falls back to a default rate, so an exempt operation stays exempt instead of acquiring a phantom 21%. If you send more lines than the original had, the extra ones have no counterpart: they carry no product and their taxation defaults to zero. **`partial`** — adjustment lines. There is no original line to match by index, so defaults are zero and `product_id` travels only if you declare it explicitly. A line without `product_id` does not move inventory. <Callout type="info"> Index-based inheritance assumes the corrected lines arrive **in the same order** as the original ones. Reordering or dropping lines crosses the inherited defaults. When your caller reorders, declare the fields explicitly per line instead of relying on inheritance. </Callout> Disbursement lines are inherited from the original as well, which is why the corrective payload accepts `line_type` and `source_invoice_reference` on a line. See [Disbursements](/guides/disbursements). ## What appears on the PDF [#pdf] The corrective is printed as a separate document with its own number, derived from the original: `SERIE-YYYY-NNN-REC{n}`, where `{n}` counts the correctives already issued against that original ([`BR-INV-021`](#traceability)). Its recipient and issuer blocks are frozen **at its own issue time**, not copied from the original. That is deliberate: a common reason to correct is precisely that the recipient's data were wrong, so the corrective must print the corrected ones ([`BR-INV-024`](#traceability)). Like any issued invoice from a VeriFactu company, it carries the legal QR block. ## What reaches the AEAT [#aeat] **As a VeriFactu record**, the corrective is an ordinary registration whose `invoice_type` is the R-code. Its tax breakdown carries the sign described in [Substitution or differences](#nature): negative base and quota for a downward correction by differences, always non-negative for a substitution. The substitution additionally declares the rectified base and quota of the original; a correction by differences does not, in line with the AEAT schema ([`BR-VFC-033`](#traceability)). **In the quarterly VAT return**, a correction of a general-regime operation lands in boxes `[14]` and `[15]` **with its sign**: a downward correction subtracts, an upward one adds. The fiscal snapshot preserves the real code (`R1`–`R4`) rather than collapsing every corrective to `R5` ([`BR-TXR-019`](#traceability)). That routing applies to the general regime only. A corrective whose header operation regime is something else follows that regime's own boxes — reverse charge and exempt or export operations are declared elsewhere and therefore do **not** reach `[14]`/`[15]`. See [Regime keys](/guides/regime-keys) for how the header regime is determined. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-INV-001` — a corrective must reference an issued original; the eligible statuses. * `BR-INV-017` — the correction nature is exactly `S` or `I`. * `BR-INV-018` — the mapping from reason slug to `R1`–`R4`. * `BR-INV-019` — a corrective of an `F2` is born `R5`. * `BR-INV-021` — the `-REC{n}` numbering of correctives. * `BR-INV-024` — the immutable recipient snapshot, frozen at the corrective's own issue time. * `BR-INV-035` — explicit `correction_code`, the AEAT legal matrix, and `justification`. * `BR-INV-036` — how corrective lines are generated and what is inherited by index. * `BR-VFC-033` — negative base and quota admitted only in corrections by differences. * `BR-TXR-019` — the sign in boxes `[14]`/`[15]` and the preservation of the real R-code. --- # Disbursements (/guides/disbursements) A *suplido* is a sum paid **in the name and on behalf of the customer**, under their express mandate (art. 78.Tres.3 of the Spanish VAT act). It is not part of what you charge for your service: you advance it, you pass it on at cost, and it never becomes your taxable base. Invoiced as an ordinary line, the same amount inflates your taxable base, your output VAT, the total you declare to the AEAT and the base you report for that customer in the annual third-party return (**Modelo 347**). Invoiced as a disbursement, it appears on the document, the customer pays it, and it stays outside all four. ## When this applies [#when] Only on **issued invoices**. Quotes, pro-formas, delivery notes, purchase invoices and recurring templates do not model disbursements at all — their line tables have no such column ([`BR-INV-037`](#traceability)). A recurring template in particular could not carry the mandatory origin reference, so the line would silently degrade into an ordinary operation and every generated invoice would declare it as your own revenue. Two further restrictions: * **A simplified invoice cannot carry one.** The mandatory content of a simplified invoice does not identify the recipient, so it cannot prove on whose behalf the amount was paid, and the tax authority would treat it as your taxable base. Its corrective is refused for the same reason. Issue a complete invoice or drop the line ([`BR-INV-040`](#traceability)). * **An invoice cannot be made of disbursements alone.** At least one ordinary line is required ([`BR-INV-046`](#traceability)). <Callout type="warn"> The API can enforce only one of the three legal conditions — that you can justify the amount. **Express mandate from the customer** is a documentary requirement Factuarea neither asks for nor stores: without it the amount is not a disbursement, however the invoice labels it. And **the input VAT on a disbursement is not deductible by you** — the customer is the real recipient of that operation. Nothing in the product stops you from deducting it, so this one is on you. </Callout> ## What the API sends [#api] Four optional line fields on [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create), [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update) and [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective): | Field | Rules | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `line_type` | `NORMAL` or `SUPLIDO`. Absent or `null` means `NORMAL`, so omitting it reproduces the previous behaviour exactly. | | `source_invoice_reference` | **Required** on a disbursement line. Free text, up to 100 characters. | | `source_invoice_ids` | Optional traceability: purchase invoices **of your own company**, validated with tenant scope. An empty list collapses to null. | | `line_total` | Optional input checksum — see [The line checksum](#checksum). | ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Honorarios de constitución de sociedad", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }, { "description": "Tasa del Registro Mercantil", "quantity": 1, "unit_price": 150, "line_type": "SUPLIDO", "source_invoice_reference": "RM-2026-0451" } ] }' ``` `POST /v1/invoices` has no `type` field, so it cannot issue a simplified invoice; the simplified-invoice refusal is therefore reachable only through the corrective endpoint on a simplified original. See [Simplified or full invoices](/guides/simplified-vs-full-invoices#f2-not-in-v1). ### The origin reference is mandatory, and it is text [#reference] It is free text rather than a foreign key because the supporting document — a court fee, a registry duty, a visa — is rarely registered as a purchase invoice in Factuarea. Without it you cannot prove the expense belongs to the customer ([`BR-INV-038`](#traceability)). `source_invoice_ids` is the optional structured counterpart, and the rule of thumb is worth internalising: > **If the supporting document is in your name, it is not a disbursement.** > Invoice it as an ordinary line. The canonical disbursement has the document issued to the **customer**, so it is not a purchase of yours and the list stays empty. Link purchase invoices only when you genuinely registered the payment in your own books as support for the advance — and remember that the input VAT on such an invoice must not be deducted. ### A disbursement line carries no tax of its own [#no-tax] Eight fields are refused on a `SUPLIDO` line with a non-zero or non-null value ([`BR-INV-039`](#traceability)): | Field | Why | | ------------------ | ----------------------------------------------------------------------------------------------- | | `tax_rate` | A disbursement is not consideration — you charge no VAT on it. | | `retention_rate` | There is no income of yours to withhold against. | | `surcharge_rate` | The equivalence surcharge taxes a supply of yours; this is not one. | | `discount_percent` | Discounting an amount paid on someone's behalf distorts it — what you pass on is what you paid. | | `regime_key` | A regime key qualifies an operation of yours. | | `exemption_reason` | A disbursement is neither taxed nor exempt: it is not your operation. | | `product_id` | It is not a supply of your goods and must not move stock. | | `pack_id` | Same reason — a pack expands into your own supplies. | The error names the offending field, and carries it as `offending_field` in the error details. Because a disbursement line cannot reference a product, the stock ledger ignores it **by construction**: the persisted row has no product and is already filtered out. ### The line checksum [#checksum] `lines[].line_total` is an **optional input** checksum. When present it is compared against the total the engine has just calculated, and the request is rejected if the deviation exceeds one cent ([`BR-INV-044`](#traceability)). The error details carry the `expected` and `received` values so you can locate a rounding mismatch with your ERP without parsing the message. Three properties, all deliberate: * **Never persisted, never returned.** There is no such column and no resource emits it. The amount invoiced is always the one Factuarea computes. * **Never mandatory**, in any scenario. Requiring it would force you to replicate our calculation engine, which is explicitly out of scope. * **The one-cent tolerance is inclusive.** A deviation of exactly 0,01 € passes; 0,02 € fails. The comparison is done in arbitrary-precision arithmetic, not floating point — float error is precisely what this field exists to diagnose. ### Errors [#errors] All `422`: | `subcode` | Cause | | ------------------------------------------- | ------------------------------------------------------------------- | | `suplido_requires_source_invoice_reference` | The disbursement line has no origin reference. | | `suplido_line_cannot_carry_taxes` | One of the eight forbidden fields was sent. | | `suplido_not_allowed_in_simplified_invoice` | A simplified invoice or its corrective. | | `invoice_requires_at_least_one_line` | Every line is a disbursement, so the invoice declares no operation. | | `line_total_checksum_mismatch` | The declared line total deviates by more than one cent. | The index in the message is zero-based over the complete line collection, so it matches the `lines.{i}` path of your payload. ## What the totals look like [#totals] The totals calculator partitions lines by type ([`BR-INV-041`](#traceability)): | Field | Contents | | ---------------------------------- | ---------------------------------------------------------------- | | `subtotal`, `taxes_total`, `total` | Ordinary lines only. The formula is untouched. | | `total_disbursements` | The sum of the disbursement lines, and **only** that. Persisted. | | `total_to_pay` | `total + total_disbursements`. **Derived**, never stored. | For the invoice above: subtotal 1000, VAT 210, total 1210, disbursements 150, amount to pay 1360. There is exactly one place in the code where those two terms are added, and every consumer — API resources, the PDF, the public document link — reads the derived value instead of recomposing the sum. Two columns called "total" would drift. **Every per-invoice figure that measures debt uses the payable amount, not the fiscal total** ([`BR-INV-045`](#traceability)): `pending_amount` is `total_to_pay − paid_amount`, the payment ledger accepts a payment covering the full payable amount without answering "exceeds pending", the transition to `paid` needs the payable amount covered — paying only the fiscal total leaves the invoice unpaid with the disbursement outstanding — and the three online payment links charge the payable amount. The **aggregate** portfolio figures are the documented exception: they measure invoiced volume rather than amount owed. That boundary, and the one affecting the Facturae and UBL documents, are listed in [Scope and limitations](/guides/scope-and-limitations#gaps). An invoice without disbursements has `total_disbursements: 0` and `total_to_pay == total`, to the cent, including every historical invoice. ## What appears on the PDF [#pdf] The disbursement **is** printed — the customer paid it and the invoice is the legal representation of that — but marked as what it is ([`BR-INV-042`](#traceability)): the line shows a dash in the VAT column, and the totals block gains a *Suplidos* row and a *Total a pagar* row below the fiscal total. The public document link shows the same. The line-level spreadsheet export adds a line-type column, because without it a disbursement is **indistinguishable from a 0% VAT operation** and summing the line-total column would give the amount collected rather than the declarable revenue. Two presentation-only line fields help here and have no fiscal effect at all ([`BR-INV-043`](#traceability)): `unit`, a free-text unit of measure printed next to the quantity, and `exemption_reason_text`, free text printed under the description for the exemption wording when the catalogued cause does not cover it. ## What reaches the AEAT [#aeat] **Nothing.** A disbursement line never reaches the VeriFactu billing record: not in the tax breakdown, not in the declared total ([`BR-VFC-036`](#traceability)). The exclusion happens at a **single point**, the read gateway, upstream of the breakdown builder — so the same filtered set feeds every consumer: the line array, the aggregate VAT rate, the operation description, the regime key and the XML generator. Filtering only the line array would have left the other paths open: a disbursement in first position donated a 0% rate to the aggregate of an invoice that does charge VAT, and described the operation to the AEAT as "Registry fee…". The declared total is unchanged in formula and excludes disbursements by construction, because the fiscal total aggregates ordinary lines only. The AEAT validates that total against the sum of the breakdown; adding the disbursement would unbalance the record and get it rejected. The amount to pay is presentation and is **never transmitted**. **In the annual third-party operations return**, the base declared for each counterparty is ([`BR-TXR-023`](#traceability)): ``` base = total invoiced (VAT included) + IRPF withholding − disbursements ``` The withholding **adds** — the counterparty received an invoice for the gross amount — and the disbursement **subtracts**, because you only passed it on for your customer's account. Inverting either sign misdeclares the counterparty. While the disbursement term was a hard-coded zero, the return **over-declared** every customer to whom fees or duties had been passed on, risking a mismatch against their own cross-declaration. Purchase invoices model neither withholding nor disbursements, so both terms are structurally zero on the received side. Whether a counterparty is declared at all is decided **on the contact**, not on the invoice. `accumulate_347` on the customer — writable over v1 on [`POST /v1/clients`](/api-reference/clients/public-api.v1.clients.create) and [`PUT /v1/clients/{id}`](/api-reference/clients/public-api.v1.clients.update), default `true` — excludes every operation of that customer when set to `false`, and it is read live when the return is computed rather than frozen at issue time ([`BR-TXR-037`](#traceability)). The older per-invoice flag survives as a **dormant override**, exposed read-only in the v1 invoice object as `exclude_347`: it can force the exclusion of a single invoice, never re-include a counterparty already marked as non-accumulating, and the public API does not set it ([`BR-TXR-024`](#traceability)). Neither flag re-includes what the automatic rules already excluded — intra-community operations, exports, and simplified invoices with no tax ID. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-INV-037` — the closed `NORMAL|SUPLIDO` line-type catalogue, its backward-compatible default, and why it exists only on issued invoices. * `BR-INV-038` — the mandatory origin reference, the optional purchase-invoice traceability, and the two legal conditions the software cannot enforce. * `BR-INV-039` — the eight fields a disbursement line may not carry. * `BR-INV-040` — no disbursements in a simplified invoice or its corrective. * `BR-INV-041` — disbursements outside base, VAT and total; the persisted aggregate and the single derived payable-amount formula. * `BR-INV-042` — which surfaces exclude the disbursement and which show it marked. * `BR-INV-043` — `unit` and `exemption_reason_text` as presentation-only fields. * `BR-INV-044` — `line_total` as an optional, never-persisted input checksum with an inclusive one-cent tolerance. * `BR-INV-045` — outstanding balance measured against the payable amount. * `BR-INV-046` — an invoice may not consist of disbursements alone. * `BR-VFC-036` — disbursements never reach the billing record, and the identical fingerprint invariant for invoices without them. * `BR-TXR-023` — the third-party return base: invoiced total plus withholding minus disbursements. * `BR-TXR-037` — accumulation in that return decided on the contact, read live, with the per-invoice flag reduced to a dormant override. * `BR-TXR-024` — the per-document exclusion flag, superseded by `BR-TXR-037` and preserved as that override. --- # Employee seat billing (/guides/employee-seats) Employees are billed through a **per-seat add-on**, not the plan's `users` limit — an employee **never** counts against that limit. The add-on is a **dedicated monthly subscription** (`employee-seats`), fully separate from your plan subscription: its `quantity` tracks the number of **active employees**, and contracting it activates the `control_horario` module. All endpoints live under `https://api.factuarea.com/v1` and use `employees:read` (status, preview) or `employees:write` (subscribe, change quantity, cancel). ## How seats are billed [#model] A **paid seat covers the whole billing period**. The seat count follows your active roster automatically: * **Activating or hiring** an employee whose seat is not covered charges a prorated seat for the rest of the period. * **Deactivating** an employee releases the seat **without a credit** (the period is already paid) but keeps their coverage, so **reactivating** them within the same period is **free**. * Each **period renewal** refreshes the coverage of the currently active employees. The quantity is kept in sync with the real active count through employee events and an hourly reconciliation, so you rarely need to set it by hand. <Callout type="info"> For an enterprise account billed **by contract** (no Stripe subscription), the add-on is granted for free: no charge, no payment method required, and the `control_horario` module is enabled all the same. Cancelling withdraws the module immediately. </Callout> ## Check the billing status [#status] `GET /v1/employee-seats` returns the add-on status: whether the subscription is active (`subscribed`), how many seats are billed (`quantity`), how many employees are active, and the recurring per-seat cost including VAT. **Amounts are in cents (minor units)** and are `null` — never a misleading `0` — when the cost is not resolvable (not subscribed, no active plan, enterprise outside Stripe, sandbox). ```bash curl https://api.factuarea.com/v1/employee-seats \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ## Preview the charge [#preview] `GET /v1/employee-seats/preview` returns the **prorated** per-seat amount for activating or hiring, computed from the Stripe upcoming invoice, **without charging**. It never throws — it degrades to a neutral preview. | Parameter | Notes | | -------------- | ---------------------------------------------------------------------------------------------------------- | | `count` | Batch preview for N seats (≥1, up to 1000). | | `employee_ids` | Coverage-aware preview by UUID v7: employees still covered this period cost `0` (`already_covered: true`). | `amount` is the taxable base in cents; `requires_payment_method` is `true` when no payment method is on file. ```bash curl -G https://api.factuarea.com/v1/employee-seats/preview \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "count=3" ``` ## Subscribe to the add-on [#subscribe] `POST /v1/employee-seats/subscribe` opts in: it creates the monthly `employee-seats` subscription with `quantity` set to your active employees and charges the first period with the payment method on file. The charge is **atomic** — if it does not go through, **nothing** is subscribed: * No payment method → `402 employee_seat_payment_method_required`; the error envelope carries `error.details.payment_setup_url` to complete card setup. * A declined charge → `402 employee_seat_charge_failed`. ```bash curl -X POST https://api.factuarea.com/v1/employee-seats/subscribe \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` <Callout type="warn"> If subscribe returns `402 employee_seat_payment_method_required`, send the user to the `payment_setup_url` from the error, let them add a card, then retry. Nothing is charged or subscribed until the first period succeeds. </Callout> ## Sync the quantity and cancel [#manage] `POST /v1/employee-seats/change-quantity` reconciles the billed seat count to the real number of active employees (a `SET` with no proration and no invoice). It is **idempotent** — a no-op when the quantity already matches. `POST /v1/employee-seats/cancel` cancels the add-on **at period end**: the current month is already paid, so `subscribed` stays `true` until the period ends, and the per-employee coverage is then purged. The **plan subscription is never touched**. ```bash curl -X POST https://api.factuarea.com/v1/employee-seats/cancel \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` See the schemas in the [API Reference](/api-reference/employees/public-api.v1.employee-seats.status). ## Typical flow [#flow] 1. **Preview** the charge for the seats you are about to activate. 2. **Subscribe** to the add-on (first period charged atomically). 3. Add or remove employees — the **quantity auto-syncs**; reconcile explicitly with change-quantity if needed. 4. Read **status** to show seats billed and per-seat cost. 5. **Cancel** at period end when you no longer need it. ## Next steps [#next] * [Time tracking overview](/guides/workforce-overview) — the portal-only employee role and the whole system. * [Managed companies](/guides/companies) — per-seat billing for gestoría child companies. --- # Error handling (/guides/errors) Every error response from the public API uses a consistent JSON envelope. The HTTP status indicates the general category; the `type` field disambiguates and the `code` field points to the specific cause. ## Envelope [#envelope] ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "El campo client_id es obligatorio.", "param": "client_id", "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA", "doc_url": "https://docs.factuarea.com/guides/errors#parameter_invalid" } } ``` Fields: * `type` — general error category. Stable and enumerated (list below). * `code` — specific cause. Stable and enumerated. * `subcode` — optional. Present where `code` alone is ambiguous: on `409` duplication conflicts it pinpoints the duplicate key (e.g. `subcode: "tax_id_already_exists"`), and on `402` payment errors it pinpoints which gate rejected the call (e.g. `subcode: "webhooks_addon_required"`). Like `code`, it is stable and invariant across languages and API versions. * `message` — human text **in Spanish**. **Not** guaranteed stable across versions; useful for logging and display. * `param` — optional, present in validation errors. Points to the **first** offending field. On multi-field validation errors the full set lives in `errors[]` (see below). * `errors[]` — optional, present on `422` validation errors. Lists **all** failed fields (see [Multi-field validation errors](#multi-field-validation-errors)). * `details` — optional. Carries `existing_resource_id` on `409` duplication conflicts (see [Duplication conflicts](#duplication-conflicts)) and `payment_setup_url` on the `402` errors that need a payment method registered (see [payment\_required\_error](#payment_required_error)). * `doc_url` — optional. Link to this guide with anchor to the specific `code` (`#{code}`). * `request_id` — unique request identifier (`req_<ULID>`). Always include it when contacting support. It is also returned in the `X-Request-Id` response header. The `error` object always carries `type`, `code` and `message`; the remaining fields are present when relevant. ## Multi-field validation errors [#multi-field-validation-errors] A `422` validation error reports **every** failed field, not just the first. The flat `param`/`message` still mirror the first field (for backward compatibility), and `errors[]` carries one item per failed field — so you fix all of them in a single round-trip instead of one request per field. ```json { "error": { "type": "invalid_request_error", "code": "invalid_param_value", "message": "El campo client_id es obligatorio.", "param": "client_id", "errors": [ { "param": "client_id", "code": "parameter_missing", "message": "El campo client_id es obligatorio." }, { "param": "issue_date", "code": "parameter_invalid_format", "message": "El formato de la fecha no es válido.", "expected_format": "YYYY-MM-DD" }, { "param": "status", "code": "parameter_invalid_enum", "message": "El valor no es válido.", "allowed_values": ["draft", "sent", "paid"] } ], "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA", "doc_url": "https://docs.factuarea.com/guides/errors#invalid_param_value" } } ``` Each item of `errors[]` carries: * `param` — the failed field name. * `code` — a stable machine-readable code derived from the failed validation rule (e.g. `parameter_missing`, `parameter_invalid_format`, `parameter_invalid_enum`, `parameter_invalid_integer`). * `message` — human-readable description of the field error. * `expected_format` — optional. Present only on format errors; the expected pattern (e.g. `YYYY-MM-DD`, `uuid`, `email`, `url`). * `allowed_values` — optional. Present only on enum errors; the list of legal values. `errors[]` is purely additive — integrations that only read `param`, `code` and `message` keep working unchanged. ## Duplication conflicts [#duplication-conflicts] A `409` duplication conflict (`code: resource_already_exists` with a `subcode` of `tax_id_already_exists`, `external_id_already_exists` or `sku_already_exists`) returns the id of the pre-existing resource in `details.existing_resource_id`. Resolve it with a single `GET` instead of an extra `find_by_*` lookup. ```json { "error": { "type": "conflict_error", "code": "resource_already_exists", "subcode": "tax_id_already_exists", "message": "Ya existe un cliente con ese NIF.", "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" }, "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM", "doc_url": "https://docs.factuarea.com/guides/errors#resource_already_exists" } } ``` A `GET /v1/clients/0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40` returns the existing resource (`200`). The same applies on `PUT` when an `external_id` already belongs to another resource of the company. ## Problem Details (RFC 9457) [#problem-details-rfc-9457] Send `Accept: application/problem+json` to receive the same error as an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details document with `Content-Type: application/problem+json`. With `Accept: application/json`, `Accept: */*` or no `Accept` header you get the flat envelope above. ```json { "type": "https://docs.factuarea.com/errors/resource_already_exists", "title": "Resource already exists", "status": 409, "detail": "Ya existe un cliente con ese NIF.", "instance": "/v1/clients", "code": "resource_already_exists", "subcode": "tax_id_already_exists", "details": { "existing_resource_id": "0193e2a1-7c4e-7b3a-9f21-2d6c8e5a1b40" }, "request_id": "req_01HKQS5NKW1C6W9T4G5HAIBZVM" } ``` * `type` — the documentation page of that specific `code`, e.g. `https://docs.factuarea.com/errors/resource_already_exists`. The `code` is the only variable segment, so you can build and compare the URI yourself. It used to be a single URI shared by every problem; if you match on it, match on `code` instead — that one never moves. * `title` — a short human summary of the problem type. * `status` — the HTTP status code. * `detail` — the human-readable message. * `instance` — the path of the affected resource. The problem+json variant **does not drop** any extension data: `code`, `subcode`, `param`, `errors[]`, `details`, `doc_url` and `request_id` are preserved as RFC 9457 extension members. ## Localized messages [#localized-messages] The `message` (and the problem+json `detail`) is localized via the `Accept-Language` header for the codes in the stable catalog. Supported languages are `es`, `en` and `ca`, with fallback to `es` when the header is absent or requests an unsupported language. The `code` and `subcode` are **invariant** across languages — always branch on `code`, never on `message`. ``` Accept-Language: en → message in English Accept-Language: ca-ES → message in Catalan (absent / Accept-Language: de) → message in Spanish (fallback) ``` Dynamic messages emitted by domain exceptions stay in Spanish; only the stable catalog messages are localized. ## Error types [#error-types] | type | HTTP | Description | | --------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `invalid_request_error` | `400` or `422` | Malformed payload, missing/invalid parameters or business validation failure. | | `authentication_error` | `401` | The API key is missing, invalid, revoked, expired or the IP is not in the allowlist. | | `payment_required_error` | `402` | The operation charges money and the payment could not go through: no payment method on file, the charge was declined, or it needs a subscription or add-on that isn't contracted. | | `authorization_error` | `403` | The key is valid but the scope doesn't cover the endpoint. | | `permission_error` | `403` | The company's plan does not grant access to the feature. | | `not_found_error` | `404` | The requested resource doesn't exist or doesn't belong to the key's company. | | `conflict_error` | `409` | Creation conflict, idempotency lock or duplicate resource (e.g. a `tax_id` already registered). | | `idempotency_error` | `409` | Reusing `Idempotency-Key` with a different payload. | | `rate_limit_error` | `429` | Exceeded the per-minute or monthly quota, or too many auth failures. | | `api_error` | `500` | Unexpected backend error. Retries may help; report to support with `request_id`. | | `service_unavailable_error` | `503` | Public API disabled via kill-switch, or downstream outage (Stripe, mailer). | <Callout type="info"> **`402` and `403` are not interchangeable.** A `402` (`payment_required_error`) means the operation is available to you and the only thing between you and it is money: register a payment method, fix the declined charge, or contract the plan or add-on it bills to. A `403` means access itself is denied — either the key lacks the scope (`authorization_error`) or the company's plan doesn't include the feature (`permission_error`) — and no payment retry changes that. The pair `addon_required` (`402`, the add-on isn't contracted) and `addon_not_active` (`403`, no key reaches a feature that isn't subscribed) is the one worth reading twice. </Callout> <Callout type="info"> **Business rule violations** (invalid status transition, an action not allowed in the current document state) respond `422` with `type: invalid_request_error` and `code: invalid_status_transition` — **not** `409`. `409 conflict_error` is reserved for duplicate creation, idempotency conflicts and concurrency locks. </Callout> ## Code catalog [#code-catalog] The anchor of each H3 heading matches exactly the value of the envelope `code` field. The `doc_url` returned by the API resolves to the specific section. The list below covers the codes you will encounter in practice; the live OpenAPI reference documents the exact codes per endpoint. <Callout type="info"> For the **complete** reference of every error `code` grouped by bounded context, with its HTTP status and type, see [All error codes](/guides/errors/all). </Callout> ### invalid\_request\_error [#invalid_request_error] ### parameter\_invalid [#parameter_invalid] A request parameter is missing or invalid. `param` points to the offending field (e.g. `client_id`, `lines[0].quantity`). ### parameter\_invalid\_format [#parameter_invalid_format] A value's format is incorrect for its semantics (regex, length, encoding, a malformed UUID, an out-of-format date). ### parameter\_invalid\_range [#parameter_invalid_range] A numeric or date value is outside the allowed range (e.g. `limit` outside `1..100`). ### parameter\_invalid\_cursor [#parameter_invalid_cursor] The `starting_after` / `ending_before` cursor is not a valid resource `id`. See [Pagination](/guides/pagination). ### parameter\_unknown [#parameter_unknown] The body contains an undocumented field (on strict endpoints). ### invalid\_param\_format [#invalid_param_format] Format constraint failed on a typed field — e.g. the `Idempotency-Key` header or the `Factuarea-Version` header is malformed. ### invalid\_param\_value [#invalid_param_value] The value does not meet a constraint (enum, format, semantic rule). ### invalid\_period [#invalid_period] The requested reporting period is invalid (e.g. a quarter/year that does not exist). ### invalid\_status\_transition [#invalid_status_transition] The requested transition is forbidden by the document state machine (e.g. sending an invoice that is not in a sendable state). Business rule violations like this are `422`, not `409`. ### invoice\_already\_paid [#invoice_already_paid] `mark-paid` on an already-paid invoice. ### quote\_already\_accepted [#quote_already_accepted] Action that conflicts with a quote already accepted. ### business\_rule\_violation [#business_rule_violation] A domain invariant blocked the operation. The `subcode` pinpoints the rule and `param` the offending field. Used by the payment ledger ([Recording payments](/guides/payments)): * `payment_exceeds_pending_amount` (`param: "amount"`) — the payment amount is greater than the invoice's pending balance. Applies to both `POST /v1/invoices/{id}/payments` and `POST /v1/purchase_invoices/{id}/payments`. * `invalid_payment_date` (`param: "paid_on"`) — the payment date is outside the allowed `issue_date … today` window (purchase invoices). * `purchase_invoice_not_payable` (`param: "status"`) — the purchase invoice is cancelled and no longer accepts payments. ```json { "error": { "type": "invalid_request_error", "code": "business_rule_violation", "subcode": "payment_exceeds_pending_amount", "message": "El importe del pago (1.500,00 €) supera el importe pendiente de la factura (710,00 €).", "param": "amount", "doc_url": "https://docs.factuarea.com/guides/errors#business_rule_violation", "request_id": "req_..." } } ``` ### unsupported\_format [#unsupported_format] The requested export/report format is not supported. ### insufficient\_data\_for\_report [#insufficient_data_for_report] Not enough data to generate the requested tax report. ### signature\_payload\_too\_large [#signature_payload_too_large] The delivery-note signature image exceeds the maximum size. ### authentication\_error [#authentication_error] ### missing\_api\_key [#missing_api_key] No authentication header present (`Authorization: Bearer` or `X-API-Key`). ### invalid\_api\_key [#invalid_api_key] The key does not exist or the secret doesn't match the stored hash. ### api\_key\_revoked [#api_key_revoked] The key was revoked. Create a new one in the dashboard. ### too\_many\_auth\_failures [#too_many_auth_failures] Repeated authentication failures from your client have been throttled. Back off and verify your credentials. ### payment\_required\_error [#payment_required_error] Every `402` comes from an operation that bills something at the moment you call it — a managed-company seat, an employee seat, or an add-on. None of them is retryable as-is: resolve the payment first, then repeat the same request. <Callout type="info"> **Version note.** Five of these codes shipped before this category existed and were served as `invalid_request_error`. They carry `payment_required_error` from [`Factuarea-Version: 2026-09-01`](/guides/versioning) onwards: `payment_method_required`, `seat_charge_failed`, `gestoria_plan_required`, `employee_seat_payment_method_required` and `employee_seat_charge_failed`. Requests on an earlier version keep the old `type` unchanged. `error.code`, `error.subcode` and the `402` status are identical on every version — branch on `code` and you never have to think about this. </Callout> ### payment\_method\_required [#payment_method_required] `POST /v1/companies` and the activation endpoints charge a seat immediately, and the accounting firm operates in live mode with no payment method on file. The response carries `details.payment_setup_url`: open it, register a card and repeat the call. ```json { "error": { "type": "payment_required_error", "code": "payment_method_required", "message": "La gestoría no tiene un método de pago configurado: configúralo para añadir la empresa.", "details": { "payment_setup_url": "https://billing.stripe.com/p/session/live_YWNjdF8xS2ZHM0RLb0h4RXBGV3lY" }, "request_id": "req_01HKQS5NPAYMENTMETHODREQ01", "doc_url": "https://docs.factuarea.com/guides/errors#payment_method_required" } } ``` ### seat\_charge\_failed [#seat_charge_failed] The pro-rated charge for the managed-company seat was declined — card refused, authentication required, or the payment provider was unreachable. The company is **not** created when the seat isn't paid. Fix the payment method in the billing portal and retry. ### gestoria\_plan\_required [#gestoria_plan_required] The accounting firm has no active paid subscription, so there is no subscription to charge the seat to. Subscribe to a plan (or resume the cancelled one) before adding managed companies. ### employee\_seat\_payment\_method\_required [#employee_seat_payment_method_required] Creating or reactivating an employee charges a seat immediately, and the company operates in live mode with no payment method on file. Same remedy as `payment_method_required`, and the response also carries `details.payment_setup_url`. ### employee\_seat\_charge\_failed [#employee_seat_charge_failed] The pro-rated charge for the employee seat was declined. The employee is **not** activated when the seat isn't paid. Fix the payment method and retry; check with your bank if the card keeps being declined. ### addon\_required [#addon_required] The operation belongs to an add-on the company hasn't contracted — e.g. `POST /v1/webhook_endpoints` requires the Developer API add-on, whose free tier allows zero endpoints (`subcode: webhooks_addon_required`). Contract the add-on and repeat the call. Unlike [`addon_not_active`](#addon_not_active) (`403`), what's missing here is the subscription, not the scope. ### authorization\_error [#authorization_error] ### insufficient\_scope [#insufficient_scope] The key lacks the scope required by the endpoint. See the catalog at [Authentication › Scopes](/guides/authentication#scopes). ### permission\_error [#permission_error] ### feature\_not\_available\_in\_plan [#feature_not_available_in_plan] The current plan does not include the required module (e.g. `recurring_invoices`). ### addon\_not\_active [#addon_not_active] The company has no active Factuarea plan that includes public API access — for example, the 10-day trial expired or the subscription lapsed outside its grace period. Subscribe to or renew a plan to keep using the API. ### not\_found\_error [#not_found_error] ### resource\_not\_found [#resource_not_found] The resource doesn't exist or doesn't belong to your company. ### tax\_report\_not\_found [#tax_report_not_found] The requested tax report does not exist. ### conflict\_error [#conflict_error] ### resource\_already\_exists [#resource_already_exists] Attempt to create a duplicate (e.g. a `tax_id` already registered). The `subcode` (e.g. `tax_id_already_exists`) pinpoints the duplicate key. ### resource\_conflict [#resource_conflict] The operation conflicts with the current state of the resource (e.g. a concurrent modification). ### max\_api\_keys\_exceeded [#max_api_keys_exceeded] The company has reached its maximum number of active API keys. ### idempotency\_error [#idempotency_error] ### idempotency\_key\_reused [#idempotency_key_reused] Same `Idempotency-Key`, different request body. Use a new key. See [Idempotency](/guides/idempotency). ### rate\_limit\_error [#rate_limit_error] ### rate\_limit\_exceeded [#rate_limit_exceeded] You exceeded the per-minute or monthly quota of your tier. The `Retry-After` header indicates the seconds to wait. See [Rate limits](/guides/rate-limits). ### api\_error [#api_error] ### internal\_error [#internal_error] Unexpected error. Already captured on our side, but share `request_id` with support. ### service\_unavailable\_error [#service_unavailable_error] ### service\_unavailable [#service_unavailable] The public API is temporarily unavailable — globally disabled via kill-switch, in a maintenance window, or a downstream dependency (database, mailer, Stripe) is unhealthy. Retry after a short back-off. ## Typed errors with the official SDK [#typed-errors-with-the-official-sdk] The [TypeScript and PHP SDKs](/sdks) map this envelope to a typed exception hierarchy, so you branch on a class (and read `code`, `type`, `param`, `request_id`) instead of parsing JSON. Your API key is never included in any exception. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { FactuareaError, ValidationError, RateLimitError, } from "@factuarea/sdk"; try { await factuarea.invoices.create(body); } catch (error) { if (error instanceof ValidationError) { console.error(error.fields); // { client_id: ["obligatorio"], … } } else if (error instanceof RateLimitError) { console.error(error.retryAfter); // seconds to wait } else if (error instanceof FactuareaError) { console.error(error.code, error.type, error.requestId); } } ``` The hierarchy also exports `AuthenticationError`, `NotFoundError`, `ConflictError`, `ServerError` and `ConnectionError`. </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Models\Errors\ErrorThrowable; try { $factuarea->invoices->publicApiV1InvoicesCreate($body); } catch (ErrorThrowable $e) { $error = $e->container->error; echo $error->type->value; // e.g. "invalid_request_error" echo $error->code; // e.g. "parameter_invalid" echo $error->param; // e.g. "client_id" echo $error->requestId; // quote this to support } ``` </Tab> </Tabs> See [SDKs › Handling errors](/sdks#handling-errors) for the full hierarchy. The retry policy below is applied automatically by both SDKs. ## request\_id and support [#request_id-and-support] Every response includes a `request_id`. Attach it to any ticket or request to `support@factuarea.com`: ``` Subject: 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV ``` With the `request_id` we correlate logs, metrics and traces to investigate quickly. ## Retry strategy [#retry-strategy] * `4xx` except `429` → **do not retry**: the error is in the request. Fix and resend. * `429` → respect the `Retry-After` header. Implement exponential back-off with jitter. * `5xx` → exponential back-off (`2^n * 100ms`) with jitter, max 5 attempts. Stripe publishes a canonical pattern that also applies here: [stripe.com/docs/error-handling](https://stripe.com/docs/error-handling). --- # All error codes (/guides/errors/all) This is the canonical reference of **every** error `code` the public API can return, grouped by the bounded context that emits it. Each `code` is stable across versions; the `message` is for display only. The total count and grouping are generated from the live catalog. ## Account [#account] | Code | Type | HTTP | Description | | ------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------- | | [`account_not_found`](/errors/account_not_found) | `not_found_error` | 404 | The account behind the key could not be resolved, which usually means the key no longer points at a live company. | | [`api_key_already_revoked`](/errors/api_key_already_revoked) | `invalid_request_error` | 422 | The key was already revoked, and a revoked key admits no further operations: revocation is terminal. | | [`api_key_not_found`](/errors/api_key_not_found) | `not_found_error` | 404 | The identifier does not match any API key of the authenticated company. | ## Authentication [#authentication] | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ---------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`api_key_expired`](/errors/api_key_expired) | `authentication_error` | 401 | The key passed its expiry date. | | [`api_key_revoked`](/errors/api_key_revoked) | `authentication_error` | 401 | The key was revoked, and a revoked key never authenticates again — revocation is the way to cut off a leaked credential. | | [`invalid_api_key`](/errors/invalid_api_key) | `authentication_error` | 401 | The key does not match any active key. It may be mistyped, truncated, or belong to a different environment — test keys and live keys are not interchangeable. | | [`ip_not_allowed`](/errors/ip_not_allowed) | `authentication_error` | 401 | The key restricts the addresses it accepts, and the request came from one outside that list. | | [`missing_api_key`](/errors/missing_api_key) | `authentication_error` | 401 | The request carries no credentials: neither the `Authorization` header nor `X-API-Key`. | | [`origin_not_allowed`](/errors/origin_not_allowed) | `authentication_error` | 401 | The request comes from a browser origin that the key does not accept. | | [`too_many_auth_failures`](/errors/too_many_auth_failures) | `authentication_error` | 429 | Too many failed authentication attempts arrived from the same address, so it is temporarily locked out to stop credential guessing. | ## Authorization [#authorization] | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------- | --------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_not_active`](/errors/addon_not_active) | `authorization_error` | 403 | The functionality belongs to an add-on that is not active for the company right now. | | [`feature_not_available_in_plan`](/errors/feature_not_available_in_plan) | `authorization_error` | 403 | The feature is not included in the company plan. | | [`forbidden_action`](/errors/forbidden_action) | `authorization_error` | 403 | The action is blocked for this resource even though the scope is right: the resource belongs to a shared catalogue, or the change travels through a different endpoint. | | [`insufficient_scope`](/errors/insufficient_scope) | `authorization_error` | 403 | The key authenticates correctly but does not carry the scope this operation requires. Scopes are granted when the key is issued and are not widened at call time. | | [`max_api_keys_exceeded`](/errors/max_api_keys_exceeded) | `authorization_error` | 422 | The company reached the number of API keys its plan allows. | | [`max_webhook_endpoints_exceeded`](/errors/max_webhook_endpoints_exceeded) | `authorization_error` | 422 | The company reached the number of webhook endpoints its add-on tier allows. | | [`module_not_available_in_sandbox`](/errors/module_not_available_in_sandbox) | `authorization_error` | 403 | The resource belongs to a module vetoed in test mode. Sandbox never touches AEAT, banks or real billing, so those modules stay out on purpose. | | [`scope_not_allowed_by_plan`](/errors/scope_not_allowed_by_plan) | `authorization_error` | 422 | One of the requested scopes belongs to a module that the plan does not include, so the key would be born with a permission that could never be exercised. | | [`scope_not_allowed_in_sandbox`](/errors/scope_not_allowed_in_sandbox) | `authorization_error` | 422 | A test key cannot be born with scopes of modules vetoed in sandbox. | ## Clients [#clients] | Code | Type | HTTP | Description | | -------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alternative_id_type_invalid`](/errors/alternative_id_type_invalid) | `invalid_request_error` | 422 | The alternative identifier type is outside the catalogue `nif_iva`, `passport`, `country_id`, `residence_certificate`, `other_document`, `not_registered`. | | [`cannot_have_both_tax_id_and_alternative_id`](/errors/cannot_have_both_tax_id_and_alternative_id) | `invalid_request_error` | 422 | The client sends `tax_id` and an alternative identifier at the same time. Fiscal identity is one: the alternative identifier exists precisely for parties without a Spanish tax id. | | [`census_requires_tax_id`](/errors/census_requires_tax_id) | `invalid_request_error` | 422 | Census verification checks the pair name plus tax id against AEAT, and one of the two is missing. | | [`client_has_documents`](/errors/client_has_documents) | `invalid_request_error` | 422 | The client is referenced by issued documents. Deleting it would leave invoices, quotes or delivery notes without the party they were issued to, and fiscal records must remain traceable. | | [`client_import_too_large`](/errors/client_import_too_large) | `invalid_request_error` | 422 | The CSV exceeds the row limit the synchronous import accepts, since the whole file is processed within the request. | | [`client_not_found`](/errors/client_not_found) | `not_found_error` | 404 | The identifier does not resolve to any client of the authenticated company. | | [`client_requires_tax_identity`](/errors/client_requires_tax_identity) | `invalid_request_error` | 422 | The client carries no fiscal identity: neither `tax_id` nor an alternative identifier, and an invoice cannot be issued to an unidentified party. | | [`direct_debit_requires_default_bank_account`](/errors/direct_debit_requires_default_bank_account) | `invalid_request_error` | 422 | Direct debit was selected as the payment method, but the client has no default bank account to charge. | | [`tax_id_already_exists`](/errors/tax_id_already_exists) | `conflict_error` | 409 | Another client of the company already holds that tax id, and the tax id identifies the party uniquely inside a company. | ## Companies [#companies] | Code | Type | HTTP | Description | | -------------------------------------------------------------- | ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`company_inactive`](/errors/company_inactive) | `authorization_error` | 403 | The profile named in `X-Active-Profile` is one of your managed companies, but it is deactivated and cannot be operated until it comes back. | | [`gestoria_module_required`](/errors/gestoria_module_required) | `authorization_error` | 403 | The master company holds a live plan, but one without the accounting-firm module, so it cannot create or operate managed companies. | | [`gestoria_plan_required`](/errors/gestoria_plan_required) | `payment_required_error` | 402 | The accounting firm has no active paid subscription, so there is no subscription on which to charge the seat. | | [`payment_method_required`](/errors/payment_method_required) | `payment_required_error` | 402 | Adding a managed company charges a seat immediately, and the accounting firm operates in live mode with no payment method on file. | | [`seat_charge_failed`](/errors/seat_charge_failed) | `payment_required_error` | 402 | The immediate pro-rated charge for the seat was declined: the card was refused, it needs authentication, or the payment provider was unreachable. The company is not created if the seat is not paid. | ## Delivery Notes [#delivery-notes] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------- | | [`delivery_note_not_found`](/errors/delivery_note_not_found) | `not_found_error` | 404 | The identifier does not resolve to any delivery note of the authenticated company. | | [`delivery_note_section_not_editable_in_status`](/errors/delivery_note_section_not_editable_in_status) | `invalid_request_error` | 422 | The logistics section — carrier, vehicle, driver — is frozen because the delivery note is already delivered, invoiced or cancelled. | | [`driver_tax_id_requires_name`](/errors/driver_tax_id_requires_name) | `invalid_request_error` | 422 | The driver tax id was sent without the driver name, and an identifier with no name identifies nobody on the delivery document. | | [`signature_payload_too_large`](/errors/signature_payload_too_large) | `invalid_request_error` | 422 | The signature image exceeds the accepted size for the field. | ## Employees [#employees] | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------------------- | ------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`employee_seat_charge_failed`](/errors/employee_seat_charge_failed) | `payment_required_error` | 402 | The immediate pro-rated charge for the employee seat was declined: the card was refused, it needs authentication, or the payment provider was unreachable. The employee is not activated if the seat is not paid. | | [`employee_seat_payment_method_required`](/errors/employee_seat_payment_method_required) | `payment_required_error` | 402 | Adding or reactivating an employee charges a seat immediately, and the company operates in live mode with no payment method on file. | ## Events [#events] | Code | Type | HTTP | Description | | -------------------------------------------- | ----------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------- | | [`event_not_found`](/errors/event_not_found) | `not_found_error` | 404 | The identifier does not match any event of the authenticated company, or the event was purged by the 30-day retention policy. | ## Idempotency [#idempotency] | Code | Type | HTTP | Description | | ------------------------------------------------------------ | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`idempotency_key_in_use`](/errors/idempotency_key_in_use) | `idempotency_error` | 409 | Another request with the same `Idempotency-Key` is still in flight, and the result is not known yet. | | [`idempotency_key_invalid`](/errors/idempotency_key_invalid) | `invalid_request_error` | 400 | The `Idempotency-Key` does not fit the accepted format: it must be 1 to 255 printable ASCII characters. | | [`idempotency_key_reused`](/errors/idempotency_key_reused) | `idempotency_error` | 409 | That `Idempotency-Key` was already used with a different payload. The key identifies one specific operation, so reusing it for another would make replay meaningless. | ## Invoices [#invoices] | Code | Type | HTTP | Description | | -------------------------------------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`corrective_invoice_inanulable`](/errors/corrective_invoice_inanulable) | `invalid_request_error` | 422 | The invoice is itself a corrective, and correctives are never annulled: the correction chain has to stay auditable end to end. | | [`export_limit_exceeded`](/errors/export_limit_exceeded) | `invalid_request_error` | 422 | The filtered selection exceeds the 5,000-invoice cap of the export, so the file is refused up front instead of being silently truncated. | | [`invalid_correction_nature`](/errors/invalid_correction_nature) | `invalid_request_error` | 422 | `correction_nature` only accepts `S` (substitution: the corrective carries the full corrected amounts) or `I` (by difference: it carries only the delta). | | [`invalid_correction_reason`](/errors/invalid_correction_reason) | `invalid_request_error` | 422 | The correction reason is outside the closed fiscal list (`error_fundado`, `concurso`, `incobrable`, `error_importe`, `error_cliente`, `devolucion`, `descuento`, `otras`), which maps to the AEAT codes R1 to R4. | | [`invalid_invoice_id`](/errors/invalid_invoice_id) | `invalid_request_error` | 400 | The invoice reference received is not a valid identifier; it usually means an internal value slipped in where the API expects the public `id`. | | [`invalid_invoice_number`](/errors/invalid_invoice_number) | `invalid_request_error` | 422 | The invoice number does not follow the canonical format `SERIES-YYYY-NNN`, plus the `-RECn` suffix on correctives. | | [`invalid_invoice_status`](/errors/invalid_invoice_status) | `invalid_request_error` | 422 | The value sent as invoice status is outside the lifecycle catalogue (`draft`, `scheduled`, `sent`, `paid`, `overdue`, `cancelled`, `annulled`). | | [`invalid_invoice_uuid`](/errors/invalid_invoice_uuid) | `invalid_request_error` | 400 | The invoice identifier in the path or in the payload is not a valid UUID. | | [`invalid_payment_method`](/errors/invalid_payment_method) | `invalid_request_error` | 422 | The payment method is outside the closed allowlist: `bank_transfer`, `cash`, `credit_card`, `sepa_direct_debit`, `paypal`, `bizum`, `other`. | | [`invoice_already_annulled`](/errors/invoice_already_annulled) | `invalid_request_error` | 422 | The invoice was already annulled. Annulment is terminal and, with VeriFactu active, its annulment record has already reached AEAT. | | [`invoice_already_paid`](/errors/invoice_already_paid) | `invalid_request_error` | 422 | The invoice is already settled. `paid` is a terminal, accounting-closed state: the output VAT has been declared, or will be declared for the period. | | [`invoice_already_sent`](/errors/invoice_already_sent) | `invalid_request_error` | 422 | The invoice was already issued: it holds a definitive series number and, with VeriFactu active, its registration with AEAT. Issuing does not happen twice. | | [`invoice_cannot_assign_number`](/errors/invoice_cannot_assign_number) | `invalid_request_error` | 422 | A definitive number was requested for an invoice that is not a draft, or that already carries one. Series numbering is monotonic and numbers are never reassigned. | | [`invoice_invalid_status_transition`](/errors/invoice_invalid_status_transition) | `invalid_request_error` | 422 | The target status is unreachable from the current one. The lifecycle is directed: `draft` moves to `scheduled` or `sent`, `sent` to `paid`, `overdue` or `annulled`, and `paid`, `cancelled` and `annulled` are terminal. | | [`invoice_not_cancellable_in_current_state`](/errors/invoice_not_cancellable_in_current_state) | `invalid_request_error` | 422 | Cancelling withdraws a draft that is not yet fiscally binding, so it only applies while the invoice is `draft`. | | [`invoice_not_correctable_in_current_state`](/errors/invoice_not_correctable_in_current_state) | `invalid_request_error` | 422 | A corrective invoice can only be issued against an invoice that is already issued (`sent` or `paid`). A draft, a cancelled or an annulled invoice has nothing to correct. | | [`invoice_not_deletable_in_current_state`](/errors/invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Only `draft` and `cancelled` invoices can be deleted. A numbered invoice never disappears: the correlative sequence must stay auditable. | | [`invoice_not_editable_in_current_state`](/errors/invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Only a draft admits editing. Once issued, the invoice is immutable and its content is frozen along with its fiscal record. | | [`invoice_not_eligible_for_action`](/errors/invoice_not_eligible_for_action) | `invalid_request_error` | 422 | The requested action does not apply to this invoice: its type or its current state leaves it outside the scope of the operation. | | [`invoice_not_found`](/errors/invoice_not_found) | `not_found_error` | 404 | The identifier does not resolve to any invoice of the authenticated company. Invoices belonging to another company answer exactly the same way. | | [`invoice_not_modifiable_in_current_state`](/errors/invoice_not_modifiable_in_current_state) | `invalid_request_error` | 422 | The field you are changing is frozen for the current state — for instance the tax regime of an annulled invoice. | | [`invoice_not_paid`](/errors/invoice_not_paid) | `invalid_request_error` | 422 | A payment receipt was requested for an invoice with no settled payment, so there is nothing to certify. | | [`invoice_not_reschedulable_in_current_state`](/errors/invoice_not_reschedulable_in_current_state) | `invalid_request_error` | 422 | Rescheduling moves the issuing date of an invoice that is waiting in `scheduled`, and this invoice is not waiting. | | [`invoice_not_schedulable_in_current_state`](/errors/invoice_not_schedulable_in_current_state) | `invalid_request_error` | 422 | Only a draft can be scheduled: scheduling reserves a future issuing moment without consuming a series number yet. | | [`invoice_not_unschedulable_in_current_state`](/errors/invoice_not_unschedulable_in_current_state) | `invalid_request_error` | 422 | Unscheduling returns an invoice from `scheduled` to `draft`, so it only applies while it is still waiting to be issued. | | [`invoice_not_unsendable_in_current_state`](/errors/invoice_not_unsendable_in_current_state) | `invalid_request_error` | 422 | Undoing the delivery mark only applies to a `sent` invoice: it clears `sent_at` and keeps the invoice issued. | | [`invoice_requires_at_least_one_line`](/errors/invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | The invoice carries no operation line, so it has no taxable base and cannot be issued. This happens both when you send no lines at all and when every line you send is a disbursement: a disbursement is an amount paid on the customer's behalf (art. 78.Tres.3 LIVA), not an operation of your own. | | [`invoice_year_required_for_ambiguous_number`](/errors/invoice_year_required_for_ambiguous_number) | `invalid_request_error` | 422 | That invoice number exists in more than one fiscal year, so on its own it does not identify a single invoice. | | [`line_total_checksum_mismatch`](/errors/line_total_checksum_mismatch) | `invalid_request_error` | 422 | The `line_total` you declared does not match the one Factuarea computes for that line (quantity × price − discount + VAT − withholding + surcharge) and the deviation is above the one-cent tolerance. The amount that gets invoiced and reported to the tax authority is always the one computed here, so the discrepancy means your system and the issued invoice would not reconcile. | | [`line_type_invalid`](/errors/line_type_invalid) | `invalid_request_error` | 422 | The line type falls outside the closed `NORMAL` / `SUPLIDO` catalogue. An issued invoice only tells two natures apart: what you sell, which forms the taxable base and carries VAT, and a disbursement (`suplido`), money advanced in the name and on behalf of the customer, which is therefore left out of the base (art. 78.Tres.3 of the Spanish VAT Act). | | [`no_invoices_in_period`](/errors/no_invoices_in_period) | `invalid_request_error` | 422 | The quarterly operation found no invoices in the requested period, so there is nothing to package or send. | | [`payment_method_invalid`](/errors/payment_method_invalid) | `invalid_request_error` | 422 | Same closed allowlist as `invalid_payment_method`, reported when the value is rejected while reading the payment method field of the payload. | | [`reminder_not_applicable`](/errors/reminder_not_applicable) | `invalid_request_error` | 422 | The payment reminder does not apply: the invoice is not `sent` or `overdue`, there is no recipient email, the public link is missing or disabled, or another reminder went out in the last 24 hours. | | [`scheduled_for_in_past`](/errors/scheduled_for_in_past) | `invalid_request_error` | 422 | `scheduled_for` is not strictly in the future, so there is no waiting period to reserve. | | [`simplified_invoice_cannot_be_substituted`](/errors/simplified_invoice_cannot_be_substituted) | `invalid_request_error` | 422 | One invoice of the substitution list cannot be replaced: it is not simplified, it is cancelled or annulled, it belongs to another company, or it already has a substitute. | | [`simplified_invoice_not_allowed`](/errors/simplified_invoice_not_allowed) | `invalid_request_error` | 422 | The operation is not eligible for a simplified invoice: it exceeds EUR 3,000, or it is an intra-EU supply, an export, a reverse-charge operation, or the customer needs a full invoice to deduct VAT. | | [`simplified_limit_exceeded`](/errors/simplified_limit_exceeded) | `invalid_request_error` | 422 | The lines would push the simplified invoice (F2) over the absolute legal cap of EUR 3,000 VAT included. | | [`suplido_line_cannot_carry_taxes`](/errors/suplido_line_cannot_carry_taxes) | `invalid_request_error` | 422 | The disbursement line carries charges of its own: a VAT rate, withholding, equivalence surcharge, discount, regime key, exemption cause or product/pack. A disbursement is not an operation of the issuer, so charging tax on it would mean paying tax on a supply you never made, and tying it to a product would move stock you never sold. | | [`suplido_not_allowed_in_simplified_invoice`](/errors/suplido_not_allowed_in_simplified_invoice) | `invalid_request_error` | 422 | The invoice is simplified (F2) and a simplified invoice does not identify the recipient. With no identified recipient there is nobody to evidence the payment on behalf of, so the amount cannot take disbursement treatment on this invoice type. | | [`suplido_requires_source_invoice_reference`](/errors/suplido_requires_source_invoice_reference) | `invalid_request_error` | 422 | The disbursement line does not carry `source_invoice_reference`, the number of the supporting document the third party issued in the customer's name. Without that document the payment is not evidenced as made on someone else's behalf, and the tax authority would treat it as the issuer's own taxable base, with VAT charged on it. | ## Notifications [#notifications] | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------- | | [`notification_not_found`](/errors/notification_not_found) | `not_found_error` | 404 | The identifier does not match any notification of the authenticated company, or the notification fell out of the retention window. | ## Payments [#payments] | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [`invalid_payment_date`](/errors/invalid_payment_date) | `invalid_request_error` | 422 | The payment date falls outside the accepted window: it cannot precede the invoice issue date, nor be in the future. | | [`payout_reconciliation_amount_mismatch`](/errors/payout_reconciliation_amount_mismatch) | `invalid_request_error` | 422 | The confirmed amount does not match the net amount of the payout, so the reconciliation would close with a difference nobody accounts for. | | [`receipt_not_available`](/errors/receipt_not_available) | `invalid_request_error` | 422 | There is no receipt to issue because the document has no settled payment behind it. | | [`stripe_payout_already_reconciled`](/errors/stripe_payout_already_reconciled) | `invalid_request_error` | 422 | The payout was already reconciled, and reconciliation is terminal: repeating it would double-count the bank entry. | | [`stripe_payout_not_found`](/errors/stripe_payout_not_found) | `not_found_error` | 404 | The identifier does not resolve to any payout of the authenticated company. | ## Products [#products] | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------- | | [`pack_in_use`](/errors/pack_in_use) | `invalid_request_error` | 422 | The pack is referenced by issued documents, so deleting it would break their composition. | | [`pack_not_found`](/errors/pack_not_found) | `not_found_error` | 404 | The identifier does not resolve to any pack of the authenticated company. | | [`pack_share_link_failed`](/errors/pack_share_link_failed) | `api_error` | 500 | The share link for the pack could not be produced. The pack itself is unaffected. | | [`product_in_use`](/errors/product_in_use) | `invalid_request_error` | 422 | The product is referenced by issued documents or by other catalogue entries, and removing it would leave those references dangling. | | [`product_not_found`](/errors/product_not_found) | `not_found_error` | 404 | The identifier does not resolve to any product of the authenticated company. | | [`sku_already_exists`](/errors/sku_already_exists) | `conflict_error` | 409 | Another product of the company already uses that SKU, and the SKU identifies the item uniquely in the catalogue. | ## Proformas [#proformas] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_expiry_date`](/errors/invalid_expiry_date) | `invalid_request_error` | 422 | The expiry date is earlier than the issue date, or more than 365 days after it. | | [`invalid_proforma_id`](/errors/invalid_proforma_id) | `invalid_request_error` | 400 | The pro forma reference received is not a valid identifier, usually because an internal value replaced the public `id`. | | [`invalid_proforma_number`](/errors/invalid_proforma_number) | `invalid_request_error` | 422 | The pro forma number does not follow the canonical numbering format of its series. | | [`invalid_proforma_status`](/errors/invalid_proforma_status) | `invalid_request_error` | 422 | The value sent as status is outside the catalogue `draft`, `accepted`, `rejected`, `expired`, `invoiced`, `cancelled`. | | [`invalid_proforma_uuid`](/errors/invalid_proforma_uuid) | `invalid_request_error` | 400 | The pro forma identifier in the path or in the payload is not a valid UUID. | | [`proforma_already_accepted`](/errors/proforma_already_accepted) | `invalid_request_error` | 422 | The customer already accepted the pro forma, and acceptance is registered once. | | [`proforma_already_rejected`](/errors/proforma_already_rejected) | `invalid_request_error` | 422 | The pro forma is already marked as rejected. | | [`proforma_cannot_be_accepted`](/errors/proforma_cannot_be_accepted) | `invalid_request_error` | 422 | Acceptance does not apply from the current state: an invoiced, cancelled or expired pro forma no longer admits it. | | [`proforma_cannot_be_rejected`](/errors/proforma_cannot_be_rejected) | `invalid_request_error` | 422 | Rejection does not apply from the current state: once invoiced, cancelled or expired, the pro forma is closed. | | [`proforma_cannot_be_sent`](/errors/proforma_cannot_be_sent) | `invalid_request_error` | 422 | Sending by email does not apply to a pro forma in a terminal state: there is no live offer to deliver. | | [`proforma_invalid_status_transition`](/errors/proforma_invalid_status_transition) | `invalid_request_error` | 422 | The target status is unreachable from the current one: a draft can be accepted, cancelled or expire; an accepted pro forma can be invoiced, rejected or expire; invoiced, cancelled and expired are terminal. | | [`proforma_not_convertible_in_current_state`](/errors/proforma_not_convertible_in_current_state) | `invalid_request_error` | 422 | Converting into an invoice requires the customer to have accepted the pro forma; from any other state there is no agreement to bill. | | [`proforma_not_deletable_in_current_state`](/errors/proforma_not_deletable_in_current_state) | `invalid_request_error` | 422 | Only a draft pro forma can be deleted. Once it has been accepted, rejected or invoiced, it is part of the commercial trail. | | [`proforma_not_draft`](/errors/proforma_not_draft) | `invalid_request_error` | 422 | The operation only makes sense while the pro forma is a draft, and this one has already moved on. | | [`proforma_not_editable_in_current_state`](/errors/proforma_not_editable_in_current_state) | `invalid_request_error` | 422 | Only a draft pro forma admits editing. Once it is accepted, rejected, expired, invoiced or cancelled, its content is settled. | | [`proforma_not_found`](/errors/proforma_not_found) | `not_found_error` | 404 | The identifier does not resolve to any pro forma of the authenticated company. | | [`proforma_requires_at_least_one_line`](/errors/proforma_requires_at_least_one_line) | `invalid_request_error` | 422 | The pro forma has no lines, so there is no amount to put in front of the customer. | | [`public_link_expires_at_exceeds_max_days`](/errors/public_link_expires_at_exceeds_max_days) | `invalid_request_error` | 422 | The requested expiry for the public link goes beyond the maximum window your plan allows for shared documents. | ## Purchase Invoices [#purchase-invoices] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`attachment_invalid_filename`](/errors/attachment_invalid_filename) | `invalid_request_error` | 422 | The file name is not usable: it is empty, it carries path components, or it exceeds 200 characters. | | [`attachment_mime_not_allowed`](/errors/attachment_mime_not_allowed) | `invalid_request_error` | 422 | The file type is outside the accepted set: PDF, PNG, JPEG, XML and HTML. | | [`attachment_missing`](/errors/attachment_missing) | `not_found_error` | 404 | The purchase invoice exists but carries no attached file, so there is nothing to download. | | [`attachment_too_large`](/errors/attachment_too_large) | `invalid_request_error` | 422 | The file exceeds the maximum size allowed for a document attachment. | | [`cannot_attach_to_cancelled_purchase_invoice`](/errors/cannot_attach_to_cancelled_purchase_invoice) | `invalid_request_error` | 422 | The invoice is cancelled, and attaching documents to a cancelled record would alter closed documentation. | | [`invalid_purchase_invoice_id`](/errors/invalid_purchase_invoice_id) | `invalid_request_error` | 400 | The purchase invoice reference received is not a valid identifier, usually because an internal value replaced the public `id`. | | [`invalid_purchase_invoice_number`](/errors/invalid_purchase_invoice_number) | `invalid_request_error` | 422 | The invoice number is empty or does not fit the accepted format. On a purchase invoice the number is the one the supplier printed, not one Factuarea generates. | | [`invalid_purchase_invoice_uuid`](/errors/invalid_purchase_invoice_uuid) | `invalid_request_error` | 400 | The purchase invoice identifier in the path or in the payload is not a valid UUID. | | [`operation_regime_invalid`](/errors/operation_regime_invalid) | `invalid_request_error` | 422 | The operation regime is outside the catalogue `general`, `intracomunitaria`, `importacion_exportacion`, `isp`. | | [`purchase_invoice_already_exists`](/errors/purchase_invoice_already_exists) | `conflict_error` | 409 | That supplier already has a purchase invoice registered with the same number. The pair supplier plus number identifies the document uniquely and prevents recording an expense twice. | | [`purchase_invoice_not_deletable_in_current_state`](/errors/purchase_invoice_not_deletable_in_current_state) | `invalid_request_error` | 422 | Only draft and cancelled purchase invoices can be deleted. A pending or paid one is part of the expense ledger. | | [`purchase_invoice_not_draft`](/errors/purchase_invoice_not_draft) | `invalid_request_error` | 422 | The operation only applies while the purchase invoice is a draft, and this one has already been registered. | | [`purchase_invoice_not_editable_in_current_state`](/errors/purchase_invoice_not_editable_in_current_state) | `invalid_request_error` | 422 | Only a draft purchase invoice can be edited. Once registered as pending, paid or cancelled, its content backs an accounting entry. | | [`purchase_invoice_not_found`](/errors/purchase_invoice_not_found) | `not_found_error` | 404 | The identifier does not resolve to any purchase invoice of the authenticated company. | | [`purchase_invoice_requires_at_least_one_line`](/errors/purchase_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | The purchase invoice has no lines, so there is no expense nor deductible VAT to record. | ## Quotes [#quotes] | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------- | | [`quote_already_accepted`](/errors/quote_already_accepted) | `invalid_request_error` | 422 | The quote was already approved, and approval is registered once. | | [`quote_already_rejected`](/errors/quote_already_rejected) | `invalid_request_error` | 422 | The quote is already marked as rejected. | | [`quote_expired`](/errors/quote_expired) | `invalid_request_error` | 422 | The quote passed its validity date, so the offered conditions are no longer binding and it cannot be approved or converted as is. | | [`quote_not_found`](/errors/quote_not_found) | `not_found_error` | 404 | The identifier does not resolve to any quote of the authenticated company. | ## Rate Limit [#rate-limit] | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ------------------ | ---- | ---------------------------------------------------------------------- | | [`monthly_quota_exceeded`](/errors/monthly_quota_exceeded) | `rate_limit_error` | 429 | The company exhausted the monthly call quota its plan includes. | | [`rate_limit_exceeded`](/errors/rate_limit_exceeded) | `rate_limit_error` | 429 | The key sent more requests than its rate allows in the current window. | ## Recurring Invoices [#recurring-invoices] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`invalid_frequency_interval`](/errors/invalid_frequency_interval) | `invalid_request_error` | 422 | The interval is lower than 1, so the recurrence would never advance to a next run. | | [`invalid_frequency_type`](/errors/invalid_frequency_type) | `invalid_request_error` | 422 | The frequency is outside the catalogue `daily`, `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`, `custom`. | | [`invalid_holiday_handling`](/errors/invalid_holiday_handling) | `invalid_request_error` | 422 | The holiday policy is outside the catalogue `skip`, `before`, `after`, `same`. | | [`invalid_recurring_invoice_id`](/errors/invalid_recurring_invoice_id) | `invalid_request_error` | 400 | The recurrence reference received is not a valid identifier, usually because an internal value replaced the public `id`. | | [`invalid_recurring_invoice_uuid`](/errors/invalid_recurring_invoice_uuid) | `invalid_request_error` | 400 | The recurrence identifier in the path or in the payload is not a valid UUID. | | [`recurring_already_active`](/errors/recurring_already_active) | `invalid_request_error` | 422 | The recurrence is already running, so there is nothing to activate. Legacy code kept for compatibility: current endpoints report this as `recurring_invoice_already_active`. | | [`recurring_invoice_already_active`](/errors/recurring_invoice_already_active) | `invalid_request_error` | 422 | The recurrence is already running. | | [`recurring_invoice_already_cancelled`](/errors/recurring_invoice_already_cancelled) | `invalid_request_error` | 422 | The recurrence was already cancelled, and cancellation is terminal. | | [`recurring_invoice_already_paused`](/errors/recurring_invoice_already_paused) | `invalid_request_error` | 422 | The recurrence is already paused, so pausing it again changes nothing. | | [`recurring_invoice_cancelled_cannot_resume`](/errors/recurring_invoice_cancelled_cannot_resume) | `invalid_request_error` | 422 | A cancelled recurrence cannot be resumed: cancellation closes it for good, unlike a pause. | | [`recurring_invoice_cannot_run`](/errors/recurring_invoice_cannot_run) | `invalid_request_error` | 422 | The recurrence cannot generate an invoice right now: it is not running, its cycle is over, or it lacks the data an invoice needs. `error.message` states the specific reason. | | [`recurring_invoice_has_generated_invoices`](/errors/recurring_invoice_has_generated_invoices) | `invalid_request_error` | 422 | The recurrence already produced invoices, and those invoices depend on it for their traceability. | | [`recurring_invoice_not_found`](/errors/recurring_invoice_not_found) | `not_found_error` | 404 | The identifier does not resolve to any recurrence of the authenticated company. | | [`recurring_invoice_requires_at_least_one_line`](/errors/recurring_invoice_requires_at_least_one_line) | `invalid_request_error` | 422 | The recurrence has no lines, so every generated invoice would come out empty. | | [`recurring_not_active`](/errors/recurring_not_active) | `invalid_request_error` | 422 | The operation needs a running recurrence and this one is paused, completed or cancelled. Legacy code kept for compatibility with older integrations. | ## Request [#request] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`business_rule_violation`](/errors/business_rule_violation) | `invalid_request_error` | 422 | A domain invariant rejected the operation. This code carries the family; `error.subcode` names the concrete rule and `error.message` explains it. | | [`conflicting_pagination_params`](/errors/conflicting_pagination_params) | `invalid_request_error` | 422 | `starting_after` and `ending_before` travelled in the same request. They walk the collection in opposite directions, so only one of them can apply. | | [`external_id_already_exists`](/errors/external_id_already_exists) | `conflict_error` | 409 | The `external_id` you use to reconcile with your own system is already assigned to another object of the same type in this company. | | [`invalid_param_format`](/errors/invalid_param_format) | `invalid_request_error` | 422 | A legacy form request rejected the shape of a value. Migrated endpoints report the same situation as `parameter_invalid_format` or `parameter_invalid_integer`. | | [`invalid_param_value`](/errors/invalid_param_value) | `invalid_request_error` | 422 | A legacy form request rejected the value of a field. Migrated endpoints report the same situation as `parameter_invalid_enum` or `parameter_invalid_range`. | | [`invalid_status_transition`](/errors/invalid_status_transition) | `invalid_request_error` | 422 | The requested state is not reachable from the state the document is in right now. | | [`length_required`](/errors/length_required) | `invalid_request_error` | 411 | A request with a body arrived using chunked transfer encoding, without declaring its size. The API needs the length up front to reject oversized payloads before buffering them. | | [`metadata_too_many_keys`](/errors/metadata_too_many_keys) | `invalid_request_error` | 422 | The `metadata` object exceeds the limit of 50 keys per resource. | | [`metadata_value_too_long`](/errors/metadata_value_too_long) | `invalid_request_error` | 422 | One value of `metadata` exceeds 500 characters once serialised to text. | | [`method_not_allowed`](/errors/method_not_allowed) | `invalid_request_error` | 405 | The path exists but does not accept the HTTP verb used. | | [`missing_required_param`](/errors/missing_required_param) | `invalid_request_error` | 422 | A legacy form request found a required field missing. Endpoints already migrated to the canonical parsers report the same situation as `parameter_missing`. | | [`parameter_invalid`](/errors/parameter_invalid) | `invalid_request_error` | 422 | A value object built from the payload rejected the value it received. `error.subcode` names which one — tax code, country code, rate, and so on. | | [`parameter_invalid_boolean`](/errors/parameter_invalid_boolean) | `invalid_request_error` | 400 | A parameter that must be a boolean received a value outside the accepted representations (`true`/`false`, `1`/`0`). | | [`parameter_invalid_cursor`](/errors/parameter_invalid_cursor) | `invalid_request_error` | 400 | The `starting_after` or `ending_before` cursor is not a valid UUID, so it cannot point at any row of the collection. | | [`parameter_invalid_empty`](/errors/parameter_invalid_empty) | `invalid_request_error` | 400 | A parameter arrived with an empty value: an `in` filter with no items, a comparison with nothing after the operator, or an equality filter with an empty string. | | [`parameter_invalid_enum`](/errors/parameter_invalid_enum) | `invalid_request_error` | 400 | The value falls outside the closed set the parameter accepts. On listings it also covers a filter operator other than `eq`, `gte`, `lte`, `gt`, `lt`, `in` or `contains`. | | [`parameter_invalid_format`](/errors/parameter_invalid_format) | `invalid_request_error` | 400 | The value has the right type but not the shape the parameter requires: a date, an identifier pattern or a header such as `Factuarea-Version`. | | [`parameter_invalid_integer`](/errors/parameter_invalid_integer) | `invalid_request_error` | 400 | A parameter that must be a whole number received something that cannot be parsed as one, such as `limit=abc`. | | [`parameter_invalid_iso8601`](/errors/parameter_invalid_iso8601) | `invalid_request_error` | 400 | A range filter (`gte`, `lte`, `gt`, `lt`) received a value that is neither numeric nor an ISO 8601 date. | | [`parameter_invalid_range`](/errors/parameter_invalid_range) | `invalid_request_error` | 400 | A numeric parameter fell outside its accepted bounds. The usual case is `limit`, which must be between 1 and 100. | | [`parameter_invalid_string`](/errors/parameter_invalid_string) | `invalid_request_error` | 400 | A parameter that must be text received an array, an object or a value that cannot be read as a string. | | [`parameter_invalid_url`](/errors/parameter_invalid_url) | `invalid_request_error` | 400 | A field that must hold an absolute URL received a value that is not one, usually because the scheme or the host is missing. | | [`parameter_invalid_uuid`](/errors/parameter_invalid_uuid) | `invalid_request_error` | 400 | An identifier field received a value that is not a valid UUID. Every v1 resource id is a UUID. | | [`parameter_invalid_value`](/errors/parameter_invalid_value) | `invalid_request_error` | 422 | The value is syntactically correct but not admissible for this resource: outside the canonical catalogue of the field, or inconsistent with the rest of the payload. | | [`parameter_missing`](/errors/parameter_missing) | `invalid_request_error` | 400 | The endpoint requires a parameter that the request did not carry. `error.param` names it. | | [`parameter_unknown`](/errors/parameter_unknown) | `invalid_request_error` | 400 | The request carries a parameter the endpoint does not accept: a filter outside its allowlist, a `sort` field that is not sortable, or the offset-style `page` — v1 paginates by cursor. | | [`payload_too_large`](/errors/payload_too_large) | `invalid_request_error` | 413 | The request body exceeds the accepted size: 1 MB as a rule, 6 MB on the endpoints that accept files. | | [`profile_not_found`](/errors/profile_not_found) | `not_found_error` | 404 | The `X-Active-Profile` header names a company that does not exist or does not belong to the accounting-firm tree of the authenticated key. Both cases answer the same so that the API never reveals companies of other tenants. | | [`resource_already_exists`](/errors/resource_already_exists) | `conflict_error` | 409 | Creating the object would duplicate one that already exists under a unique key — tax id, SKU, external id. `error.details.existing_resource_id` points at the object that already holds the value. | | [`resource_conflict`](/errors/resource_conflict) | `conflict_error` | 409 | The operation collided with the current state of the resource and no more specific conflict code applies. | | [`resource_immutable`](/errors/resource_immutable) | `invalid_request_error` | 422 | The object is closed to changes for this operation: its state or its accounting record forbids modifying it. | | [`resource_locked`](/errors/resource_locked) | `conflict_error` | 409 | Another operation holds the resource until it finishes: concurrent writes on the same object are serialised instead of interleaved. | | [`resource_not_deletable`](/errors/resource_not_deletable) | `invalid_request_error` | 422 | The object exists but its state or its dependants block the deletion. In bulk deletions this is the per-row code of every entry that could not be removed. | | [`resource_not_found`](/errors/resource_not_found) | `not_found_error` | 404 | The identifier resolves to nothing visible to the authenticated company. Objects belonging to another company answer exactly the same way, by design. | | [`route_not_found`](/errors/route_not_found) | `not_found_error` | 404 | The path does not match any v1 endpoint. It is usually a typo, a missing `/v1` prefix, or a path from a different area of the API. | | [`unknown_filter`](/errors/unknown_filter) | `invalid_request_error` | 422 | A listing received a filter it does not know. The canonical v1 parsers report this as `parameter_unknown`; this code survives for endpoints that have not migrated yet. | | [`unsupported_api_version`](/errors/unsupported_api_version) | `invalid_request_error` | 400 | The `Factuarea-Version` header is well formed but names a version outside the supported set. | | [`unsupported_media_type`](/errors/unsupported_media_type) | `invalid_request_error` | 415 | A request with a body declared a `Content-Type` other than `application/json`. | ## Series [#series] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`cannot_archive_last_default_series`](/errors/cannot_archive_last_default_series) | `invalid_request_error` | 422 | The series is the only active one for its document type. Archiving it would leave the company with no numbering available and freeze that kind of document. | | [`document_type_required_for_ambiguous_code`](/errors/document_type_required_for_ambiguous_code) | `invalid_request_error` | 422 | That series code exists for more than one document type, so on its own it does not identify a single series. | | [`invalid_series_code`](/errors/invalid_series_code) | `invalid_request_error` | 422 | The series code is empty, too long, or carries characters that do not belong in a fiscal prefix. | | [`invalid_series_name`](/errors/invalid_series_name) | `invalid_request_error` | 422 | The series name is empty or exceeds the allowed length. | | [`invalid_series_number`](/errors/invalid_series_number) | `invalid_request_error` | 422 | The starting number is not valid: it is not a positive integer, or it falls at or below the last number already issued, which would re-issue numbers already in use. | | [`invalid_series_uuid`](/errors/invalid_series_uuid) | `invalid_request_error` | 400 | The series identifier in the path or in the payload is not a valid UUID. | | [`invalid_series_year`](/errors/invalid_series_year) | `invalid_request_error` | 422 | The fiscal year is not a valid four-digit year for a numbering series. | | [`monthly_requires_month_segmented_format`](/errors/monthly_requires_month_segmented_format) | `invalid_request_error` | 422 | The counter resets monthly but the numbering mask does not segment by month, so two months would start on the same correlative and produce duplicate numbers within the year. | | [`series_already_archived`](/errors/series_already_archived) | `invalid_request_error` | 422 | The series was already archived, and archiving is not repeated: a second call means the client is out of sync with the real state. | | [`series_code_immutable_with_documents`](/errors/series_code_immutable_with_documents) | `invalid_request_error` | 422 | Changing the prefix of a series that already issued documents would retroactively rewrite their fiscal identifier, while customers and AEAT hold the original number. | | [`series_has_documents`](/errors/series_has_documents) | `invalid_request_error` | 422 | The series already numbered documents, so it cannot be removed: the correlative sequence has to stay auditable. | | [`series_immutable`](/errors/series_immutable) | `invalid_request_error` | 405 | Series are not editable nor deletable through the API: legal numbering continuity requires their prefix, year and counter to stay put. | | [`series_initial_number_creates_gap`](/errors/series_initial_number_creates_gap) | `invalid_request_error` | 422 | The starting number jumps beyond the next natural correlative while documents already exist for the current year, and that gap in the sequence is not acceptable to AEAT. | | [`series_locked_by_verifactu`](/errors/series_locked_by_verifactu) | `invalid_request_error` | 422 | At least one invoice of the series holds a billing record accepted by AEAT, which freezes the prefix, the year and the numbering base of the series. | | [`series_not_found`](/errors/series_not_found) | `not_found_error` | 404 | The identifier does not resolve to any numbering series of the authenticated company. | | [`series_type_invalid`](/errors/series_type_invalid) | `invalid_request_error` | 422 | The document type of the series is outside the catalogue `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`series_year_locked`](/errors/series_year_locked) | `invalid_request_error` | 422 | The series already issued documents in its current year. Moving the year would leave those documents pointing at an empty year while their taxable base sits in another. | ## Server [#server] | Code | Type | HTTP | Description | | -------------------------------------------------------------- | --------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | [`dependency_unavailable`](/errors/dependency_unavailable) | `service_unavailable_error` | 503 | An external service the operation relies on did not answer in time. | | [`face_transmission_failed`](/errors/face_transmission_failed) | `api_error` | 502 | The FACe platform — the public administration entry point — was unreachable or answered with a fault. The failure is upstream, not in your request. | | [`facturae_signing_failed`](/errors/facturae_signing_failed) | `api_error` | 500 | The XAdES signature of the Facturae file could not be produced, usually because the signing certificate is unusable at that moment. | | [`internal_error`](/errors/internal_error) | `api_error` | 500 | Something broke on our side while processing the request. The condition is not caused by your payload. | | [`maintenance`](/errors/maintenance) | `service_unavailable_error` | 503 | The platform is in a maintenance window and writes are held back on purpose. | | [`pdf_generation_failed`](/errors/pdf_generation_failed) | `service_unavailable_error` | 503 | The rendering service could not produce the PDF. The document and its data are intact — what failed is the file. | | [`register_sealing_failed`](/errors/register_sealing_failed) | `api_error` | 500 | The cryptographic sealing of the record did not complete, so the closure was left unsigned rather than sealed with a broken signature. | | [`send_failed`](/errors/send_failed) | `api_error` | 500 | The document was not delivered by email: the mail provider rejected the message or was unreachable. | | [`service_unavailable`](/errors/service_unavailable) | `service_unavailable_error` | 503 | The service, or a dependency it needs, is temporarily unable to answer. | ## Suppliers [#suppliers] | Code | Type | HTTP | Description | | ---------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [`supplier_has_documents`](/errors/supplier_has_documents) | `invalid_request_error` | 422 | The supplier is referenced by registered purchase invoices, and deleting it would leave those expenses without the party that issued them. | | [`supplier_not_found`](/errors/supplier_not_found) | `not_found_error` | 404 | The identifier does not resolve to any supplier of the authenticated company. | ## Tax Reports [#tax-reports] | Code | Type | HTTP | Description | | ---------------------------------------------------------------------- | ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`insufficient_data_for_report`](/errors/insufficient_data_for_report) | `invalid_request_error` | 422 | The period has no data to file, or an invoice of the period lacks a mandatory field for this model — typically the customer tax id. | | [`invalid_period`](/errors/invalid_period) | `invalid_request_error` | 422 | The period does not identify a filing: the year is outside the accepted range, or the quarter is missing or out of the range 1 to 4 for a quarterly model. | | [`report_format_invalid`](/errors/report_format_invalid) | `invalid_request_error` | 422 | The format is outside the catalogue `txt_aeat`, `pdf`, `excel`. | | [`tax_report_not_found`](/errors/tax_report_not_found) | `not_found_error` | 404 | The identifier does not resolve to any tax report of the authenticated company. | | [`tax_report_type_invalid`](/errors/tax_report_type_invalid) | `invalid_request_error` | 422 | The report type is outside the catalogue `modelo_303`, `modelo_347`, `modelo_130`. | | [`unsupported_format`](/errors/unsupported_format) | `invalid_request_error` | 422 | The requested format is not available for this model: not every filing produces every output. | ## Taxes [#taxes] | Code | Type | HTTP | Description | | ------------------------------------------------------------------------------------------------ | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`custom_tax_creation_disabled`](/errors/custom_tax_creation_disabled) | `authorization_error` | 403 | Creating custom taxes is disabled for this company. | | [`duplicate_tax_default_for_document_type`](/errors/duplicate_tax_default_for_document_type) | `invalid_request_error` | 422 | Another tax of the same type is already the default for that document type, and the pair (tax type, document type) admits a single default. | | [`indirect_tax_regime_invalid`](/errors/indirect_tax_regime_invalid) | `invalid_request_error` | 422 | The indirect regime is outside the catalogue `iva`, `igic`, `ipsi`. | | [`invalid_aeat_code`](/errors/invalid_aeat_code) | `invalid_request_error` | 422 | The AEAT operation code is outside the closed catalogue `S1`, `S2`, `S3`, `E1`-`E6`, `N1`, `N2` used by VeriFactu and SII. | | [`invalid_country_aeat_zone`](/errors/invalid_country_aeat_zone) | `invalid_request_error` | 422 | The AEAT territorial zone is outside the catalogue `peninsula`, `canarias`, `ceuta`, `melilla`. | | [`invalid_country_code`](/errors/invalid_country_code) | `invalid_request_error` | 422 | The country code is not exactly two characters, so it is not a valid ISO 3166-1 alpha-2 code. | | [`invalid_customer_visible_label`](/errors/invalid_customer_visible_label) | `invalid_request_error` | 422 | The label shown to the customer on the document exceeds the allowed length. | | [`invalid_description`](/errors/invalid_description) | `invalid_request_error` | 422 | The description exceeds the maximum length allowed for the field. | | [`invalid_document_type`](/errors/invalid_document_type) | `invalid_request_error` | 422 | The document type is outside the catalogue: `invoice`, `quote`, `delivery_note`, `proforma`, `purchase_invoice`, `recurring_invoice`. | | [`invalid_rate_for_tax_regime`](/errors/invalid_rate_for_tax_regime) | `invalid_request_error` | 422 | The rate does not belong to the legal grid of its regime: IGIC admits 0, 3, 5, 7, 9.5, 15 and 20%; IPSI admits 0, 0.5, 1, 2, 4, 8 and 10%. | | [`invalid_tax_code`](/errors/invalid_tax_code) | `invalid_request_error` | 422 | The tax code is empty or longer than 50 characters. | | [`invalid_tax_name`](/errors/invalid_tax_name) | `invalid_request_error` | 422 | The tax name is empty or longer than 255 characters. | | [`invalid_tax_rate`](/errors/invalid_tax_rate) | `invalid_request_error` | 422 | The rate falls outside the range allowed for its type: VAT 0-27%, withholding 0-47%, equivalence surcharge 0-10%, other 0-100%. | | [`invalid_tax_type_filter`](/errors/invalid_tax_type_filter) | `invalid_request_error` | 422 | The `type` filter of the by-type listing carries a value outside the enum `vat`, `retention`, `surcharge`, `other`. | | [`invalid_validity_window`](/errors/invalid_validity_window) | `invalid_request_error` | 422 | The validity window is inverted: `valid_until` falls before `valid_from`. | | [`system_tax_default_modification_forbidden`](/errors/system_tax_default_modification_forbidden) | `authorization_error` | 403 | Defaults of the shared catalogue taxes are not set on the tax itself: the catalogue is global and the preference belongs to your company. | | [`system_tax_immutable`](/errors/system_tax_immutable) | `invalid_request_error` | 422 | The tax belongs to the canonical AEAT catalogue shipped with the product. Its rate, code and name are fixed so that every company shares the same fiscal reference. | | [`system_tax_immutable_field`](/errors/system_tax_immutable_field) | `invalid_request_error` | 422 | The update touches a field that is frozen on a system tax; `error.param` names it. | | [`system_tax_undeletable`](/errors/system_tax_undeletable) | `invalid_request_error` | 422 | System taxes are part of the shared fiscal catalogue and cannot be removed: deleting one would break the documents that reference it. | | [`tax_applies_to_invalid`](/errors/tax_applies_to_invalid) | `invalid_request_error` | 422 | The scope of the tax is outside the catalogue `sale`, `purchase`, `both`. | | [`tax_code_already_exists`](/errors/tax_code_already_exists) | `conflict_error` | 409 | Another tax of the catalogue already uses that code, and codes identify taxes unambiguously. | | [`tax_id_required`](/errors/tax_id_required) | `invalid_request_error` | 422 | The operation needs the tax identification number (NIF, CIF or NIE) of the party involved and the record does not carry one. | | [`tax_in_use`](/errors/tax_in_use) | `invalid_request_error` | 422 | The tax is referenced by documents, products or suppliers. Removing it would leave historical documents without their fiscal reference. | | [`tax_inactive_cannot_be_default`](/errors/tax_inactive_cannot_be_default) | `invalid_request_error` | 422 | A deactivated tax cannot become the default, either globally or for a document type — it would offer a hidden default that no form can pick. | | [`tax_not_found`](/errors/tax_not_found) | `not_found_error` | 404 | The identifier does not match any tax of the catalogue reachable by this company. | | [`tax_type_invalid`](/errors/tax_type_invalid) | `invalid_request_error` | 422 | The tax type is outside the catalogue `vat`, `retention`, `surcharge`, `other`. | ## VeriFactu [#verifactu] | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------------- | ----------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`alta_record_not_found`](/errors/alta_record_not_found) | `not_found_error` | 404 | The invoice has no registration record, so the operation that depends on it has nothing to work with. | | [`anulacion_record_already_exists`](/errors/anulacion_record_already_exists) | `conflict_error` | 409 | The invoice already carries an annulment record in the chain, and annulment is reported only once. | | [`certificate_expired`](/errors/certificate_expired) | `invalid_request_error` | 422 | The certificate is outside its validity window: it has expired, or it is not valid yet. | | [`certificate_nif_mismatch`](/errors/certificate_nif_mismatch) | `invalid_request_error` | 422 | The tax id of the certificate holder does not match the company tax id. AEAT records are signed on behalf of the company, so both must be the same. | | [`certificate_not_found`](/errors/certificate_not_found) | `not_found_error` | 404 | The company has no FNMT certificate matching the identifier, or none uploaded at all. | | [`certificate_too_large`](/errors/certificate_too_large) | `invalid_request_error` | 422 | The file exceeds the 100 KB limit, while a real FNMT certificate weighs a few kilobytes. | | [`clock_drift_exceeded`](/errors/clock_drift_exceeded) | `invalid_request_error` | 422 | The server clock drifted from NTP beyond the allowed margin. The generation timestamp is part of the AEAT fingerprint, so an unsynchronised clock would produce records AEAT rejects. | | [`declaracion_already_exists`](/errors/declaracion_already_exists) | `conflict_error` | 409 | The company already filed its SIF responsibility statement for that period. | | [`declaracion_not_found`](/errors/declaracion_not_found) | `not_found_error` | 404 | The company has no SIF responsibility statement filed for the requested period. | | [`event_already_processed`](/errors/event_already_processed) | `invalid_request_error` | 422 | That SIF event is already recorded in the event chain, and each event is processed exactly once. | | [`invalid_certificate_format`](/errors/invalid_certificate_format) | `invalid_request_error` | 422 | The file is not a PKCS#12 container: its first bytes do not match the ASN.1 structure the format requires, whatever its extension says. | | [`invalid_certificate_password`](/errors/invalid_certificate_password) | `invalid_request_error` | 422 | The password does not open the certificate file. | | [`max_retries_exceeded`](/errors/max_retries_exceeded) | `invalid_request_error` | 422 | The record exhausted the technical retry budget for resending the stored XML. Retrying the same content again would fail the same way. | | [`mode_switch_blocked_until_year_end`](/errors/mode_switch_blocked_until_year_end) | `invalid_request_error` | 422 | VeriFactu mode was activated during this fiscal year and at least one billing record was issued. Stepping back would degrade the integrity of a chain already reported to AEAT. | | [`record_already_accepted`](/errors/record_already_accepted) | `invalid_request_error` | 422 | AEAT already accepted the record. Acceptance is terminal and its content is frozen as part of the fingerprint chain. | | [`record_immutable`](/errors/record_immutable) | `invalid_request_error` | 422 | The record belongs to an append-only ledger: once written, its fiscal content is closed to changes and to deletion. | | [`record_not_rejected`](/errors/record_not_rejected) | `invalid_request_error` | 422 | The correction flow only applies to records AEAT rejected on data grounds. This record is in another state — a technical failure, for instance, is covered by the automatic retry. | | [`record_not_subsanable`](/errors/record_not_subsanable) | `invalid_request_error` | 422 | The record cannot be amended: it is not a registration record, or it has no source invoice from which its content could be regenerated. | | [`requires_annulment`](/errors/requires_annulment) | `invalid_request_error` | 422 | The regenerated content changes a field that takes part in the fingerprint — issuer tax id, series and number, issue date, invoice type, tax amount or total — and the chain cannot be rewritten. | | [`sii_excluded`](/errors/sii_excluded) | `invalid_request_error` | 422 | The company is registered with SII, and SII filers are excluded from the VeriFactu regulation. | | [`verifactu_already_submitted`](/errors/verifactu_already_submitted) | `invalid_request_error` | 422 | The invoice already has its registration record. Exactly one registration exists per invoice, so a second one would break the idempotency of the chain. | | [`verifactu_mode_invalid`](/errors/verifactu_mode_invalid) | `invalid_request_error` | 422 | The mode is outside the catalogue `verifactu` / `no_verifactu`. | | [`verifactu_not_eligible`](/errors/verifactu_not_eligible) | `invalid_request_error` | 422 | The invoice cannot be registered with AEAT right now: the company is not on VeriFactu mode, it has no active certificate, or the certificate is revoked or issued for a different tax id. | | [`verifactu_record_not_found`](/errors/verifactu_record_not_found) | `not_found_error` | 404 | The identifier does not match any billing record of the authenticated company. | | [`verifactu_transmission_failed`](/errors/verifactu_transmission_failed) | `invalid_request_error` | 422 | The transmission of the record to AEAT did not complete: the endpoint was unreachable or answered with an incident. | ## Webhooks [#webhooks] | Code | Type | HTTP | Description | | ---------------------------------------------------------------------------- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`addon_required`](/errors/addon_required) | `payment_required_error` | 402 | Creating webhook endpoints belongs to the Developer API add-on, and the company does not have it active — the free tier allows zero endpoints. | | [`api_version_invalid_format`](/errors/api_version_invalid_format) | `invalid_request_error` | 422 | The payload version of the endpoint is not a `YYYY-MM-DD` date. | | [`api_version_unsupported`](/errors/api_version_unsupported) | `invalid_request_error` | 422 | The payload version is well formed but is not among the ones the platform serves. | | [`custom_header_blocklisted`](/errors/custom_header_blocklisted) | `invalid_request_error` | 422 | One of the custom headers is reserved: the HTTP layer manages it (`host`, `content-type`, `content-length`, `user-agent`), Factuarea sends it as part of the signed contract (`factuarea-*`), or the proxy owns it (`x-forwarded-*`). | | [`custom_header_value_too_long`](/errors/custom_header_value_too_long) | `invalid_request_error` | 422 | The value of a custom header exceeds 1024 characters. | | [`replay_delivery_not_retryable`](/errors/replay_delivery_not_retryable) | `invalid_request_error` | 422 | Only failed deliveries can be replayed. A delivery that succeeded, or one still in flight, has nothing to resend. | | [`replay_event_expired`](/errors/replay_event_expired) | `invalid_request_error` | 422 | The event behind the delivery was purged by the 30-day retention policy, so there is no payload left to resend. | | [`timeout_seconds_out_of_range`](/errors/timeout_seconds_out_of_range) | `invalid_request_error` | 422 | `timeout_seconds` falls outside the range 1 to 30 seconds. | | [`too_many_custom_headers`](/errors/too_many_custom_headers) | `invalid_request_error` | 422 | The endpoint declares more than 20 custom headers. | | [`webhook_delivery_not_found`](/errors/webhook_delivery_not_found) | `not_found_error` | 404 | The identifier does not match any delivery attempt, or the delivery falls outside the retention window kept for the history. | | [`webhook_endpoint_degraded`](/errors/webhook_endpoint_degraded) | `invalid_request_error` | 422 | The endpoint is degraded after repeated delivery failures, so test pings are refused while it stays in that state. | | [`webhook_endpoint_not_found`](/errors/webhook_endpoint_not_found) | `not_found_error` | 404 | The identifier does not resolve to any webhook endpoint of the authenticated company. | | [`webhook_secret_recently_rotated`](/errors/webhook_secret_recently_rotated) | `rate_limit_error` | 429 | The signing secret was rotated less than five minutes ago. The grace window lets your receiver accept both secrets during the switch; rotating again inside it would invalidate signatures still in flight. | --- # Events (/guides/events) Every event published in Factuarea is **persisted** as a read-only `event` object with an opaque `id` (a UUID v7, e.g. `01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d`), consistent with the `id` of every other v1 resource. This lets you: * Query it via API: `GET /v1/events/{id}` and `GET /v1/events?type=invoice.paid`. * Deliver it to subscribed webhook endpoints (the same object is sent in the delivery body — see [Webhooks](/guides/webhooks)). * Replay a delivery from the dashboard (`Developers > Webhooks > Deliveries`). ## Payload shape [#payload-shape] Every event shares this structure: ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03", "api_version": "2026-05-22", "livemode": true, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } }, "created": "2026-05-15T10:23:18Z" } ``` Fields: * `id` — opaque identifier of the event (UUID v7). Use it as the idempotency key on your side. * `object` — always `event`. * `type` — event name as `<category>.<action>` (e.g. `invoice.paid`, `quote.approved`). * `aggregate_id` — UUID v7 of the resource that produced the event (e.g. the invoice for `invoice.paid`). `null` for events without a backfilled aggregate. Distinct from `id`, which identifies the event itself. * `api_version` — date-based version the payload was serialized under, sealed at emission. Always present on events emitted today; `null` only for legacy events emitted before versions were sealed. * `livemode` — `true` for events generated in production (live key, `fact_live_`); `false` for events generated in test mode (sandbox company, `fact_test_` key). Test-mode events are recorded and queryable via `GET /v1/events`, but **not delivered** to webhook endpoints (see [Test mode & sandbox](/guides/test-mode)), so any event your endpoint actually receives is always `livemode: true`. * `data` — a **thin reference** to the affected resource, keyed by its type — e.g. `{ "invoice": { "id": "..." } }`. Fetch the resource from its own endpoint to get the full, current representation. * `created` — ISO 8601 UTC timestamp of when the event was created. ## Idempotency [#idempotency] Each event has a unique `id`. Webhooks redeliver the same `id` to the same endpoint on every retry. In your handler: ```python event_id = event['id'] if seen_in_db(event_id): return '', 200 process(event) mark_seen_in_db(event_id) ``` ## Event catalog [#event-catalog] The full, authoritative catalog of subscribable event types is returned by `GET /v1/event-catalog`. Each entry carries a `name`, a `category`, a human-readable `description`, and a `status` (`available` or `coming_soon`): ```bash curl https://api.factuarea.com/v1/event-catalog \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` ```json { "data": [ { "name": "invoice.paid", "category": "invoice", "description": "Factura pagada", "status": "available" } ], "has_more": false, "next_cursor": null } ``` Representative event types by category (query the catalog for the complete, up-to-date list): ### Invoices [#invoices] `invoice.created`, `invoice.auto_created`, `invoice.corrective_auto_created`, `invoice.subscription_auto_created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.cancelled`, `invoice.annulled`, `invoice.overdue`, `invoice.deleted`, `invoice.number_assigned`, `invoice.rectified`, `invoice.email_sent`, `invoice.email_failed`, `invoice.payment_reminder_sent`, `invoice.simplified_created`, `invoice.simplified_substituted`, `invoice.substituted_by_complete`, `invoice.verifactu_submitted`, `invoice.verifactu_failed`, `invoice.metadata_changed`. `invoice.auto_created` / `invoice.corrective_auto_created` / `invoice.subscription_auto_created` are emitted by the [payment-gateway auto-invoicing](/payments/stripe-autoinvoicing) flows when a charge, refund or subscription cycle produces an invoice automatically. ### Quotes [#quotes] `quote.created`, `quote.updated`, `quote.deleted`, `quote.approved`, `quote.rejected`, `quote.converted`, `quote.expired`, `quote.marked_as_pending`, `quote.cancelled`, `quote.number_assigned`, `quote.metadata_changed`, `quote.email_sent`, `quote.email_failed`. ### Pro-forma invoices [#pro-forma-invoices] `proforma.created`, `proforma.updated`, `proforma.deleted`, `proforma.accepted`, `proforma.rejected`, `proforma.cancelled`, `proforma.expired`, `proforma.converted_to_invoice`, `proforma.number_assigned`, `proforma.metadata_changed`, `proforma.email_sent`, `proforma.email_failed`. ### Delivery notes [#delivery-notes] `delivery_note.created`, `delivery_note.updated`, `delivery_note.status_changed`, `delivery_note.signed`, `delivery_note.converted`, `delivery_note.email_sent`, `delivery_note.email_failed`. ### Purchase invoices [#purchase-invoices] `purchase_invoice.created`, `purchase_invoice.updated`, `purchase_invoice.paid`, `purchase_invoice.payment_registered`, `purchase_invoice.cancelled`, `purchase_invoice.metadata_changed`. ### Recurring invoices [#recurring-invoices] `recurring_invoice.created`, `recurring_invoice.activated`, `recurring_invoice.paused`, `recurring_invoice.updated`, `recurring_invoice.deleted`, `recurring_invoice.completed`, `recurring_invoice.executed`, `recurring_invoice.failed`, `recurring_invoice.cancelled`, `recurring_invoice.metadata_changed`. ### Clients & products [#clients--products] `client.created`, `client.updated`, `client.deleted`, `client.metadata_changed`, `product.created`, `product.updated`. ### Series & taxes [#series--taxes] `series.created`, `series.updated`, `series.deleted`, `series.archived`, `series.unarchived`, `series.marked_as_default`, `series.demoted_from_default`, `series.number_consumed`, `series.year_reset`, `series.month_reset`, `tax.metadata_changed`, `tax.validity_changed`, `tax.external_reference_changed`, `payment.received`. ### FacturaE (FACe) [#facturae-face] `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested`. ### Payments & gateways [#payments--gateways] `payout.reconciled`. `payout.reconciled` fires when a Stripe payout is reconciled against your bank statement (see [Payouts & reconciliation](/payments/payouts-reconciliation)). ## Payload examples [#payload-examples] ### invoice.paid [#invoicepaid] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03", "api_version": "2026-05-22", "livemode": true, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } }, "created": "2026-05-15T11:42:08Z" } ``` ### client.updated [#clientupdated] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a1a", "object": "event", "type": "client.updated", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a2b", "api_version": "2026-05-22", "livemode": true, "data": { "client": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a2b" } }, "created": "2026-05-15T11:50:12Z" } ``` ### quote.converted [#quoteconverted] ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a3c", "object": "event", "type": "quote.converted", "aggregate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d", "api_version": "2026-05-22", "livemode": true, "data": { "quote": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a4d" } }, "created": "2026-05-15T12:01:55Z" } ``` The event carries only a thin reference to the affected resource. Fetch the resource from its own endpoint (e.g. `GET /v1/quotes/{id}`) to read the converted invoice it links to. ## Subscribe to events [#subscribe-to-events] Via API: ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.mycompany.com/factuarea/webhook", "enabled_events": ["invoice.paid", "quote.approved"] }' ``` To subscribe to **all events** (not recommended in production except for internal dashboards): ```json { "enabled_events": ["*"] } ``` To subscribe to entire families (all `invoice.*`): ```json { "enabled_events": ["invoice.*", "quote.*"] } ``` ## List events via API [#list-events-via-api] ```bash GET /v1/events?type=invoice.paid&limit=50 ``` Available filters: `type`, `type[in]`, `created[gte]`, `created[lte]`, `created[gt]`, `created[lt]`. Standard cursor pagination (`limit`, `starting_after`, `ending_before`) — see [Pagination](/guides/pagination). --- # Export and import (/guides/export-and-import) Two file-based operations move data in and out of Factuarea over the public API: **export invoices** to a spreadsheet, and **import clients** from a CSV. Both reuse the same engines the dashboard uses, and the import follows the [partial-success](/docs/guides/bulk-operations) contract — one bad row never sinks the whole file. ## Export invoices to a spreadsheet [#export-invoices-to-a-spreadsheet] `POST /v1/invoices/export/excel` (scope `invoices:read`) builds an XLSX or CSV spreadsheet of your invoices and streams the binary file back. It is a **read** operation: nothing is created or modified. Two orthogonal axes control the output: | Parameter | Values | Meaning | | ------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `format` | `SUMMARY` (default) · `ITEMS` | Content layout. `SUMMARY` is **one row per invoice**; `ITEMS` is **one row per invoice line** (header columns repeated on each line). | | `file_format` | `xlsx` (default) · `csv` | File container. | Pick the invoices to export in either of two ways: * **By id** — pass `invoice_ids` with the UUID v7 ids of specific invoices. * **By filter** — omit `invoice_ids` and narrow the set with `status`, `date_from`, `date_to`, `client_id`, `series_id` and `search`. `date_from` and `date_to` filter the issue date (inclusive); `client_id` and `series_id` take the public UUID v7 of the client/series. ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/export/excel \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "format": "ITEMS", "file_format": "xlsx", "status": "paid", "date_from": "2026-01-01", "date_to": "2026-03-31" }' \ -o invoices-q1.xlsx ``` ### The 5000-invoice limit [#the-5000-invoice-limit] The selected set is capped at **5000 invoices**. If your filters match more than that, the API does **not** truncate silently — it returns `422` with the `export_limit_exceeded` error code: ```json { "error": { "type": "invalid_request_error", "code": "export_limit_exceeded", "message": "La exportación supera el máximo de 5000 facturas." } } ``` Narrow the date range, the status or the client, or split the export into several calls, so each request stays under the limit. <Callout type="info"> `client_id`, `series_id` and the `invoice_ids` entries are matched **inside your company**. A UUID that does not exist or belongs to another company is simply dropped from the selection — it never leaks cross-tenant data and never returns a global `404`. </Callout> ## Import clients from a CSV [#import-clients-from-a-csv] `POST /v1/clients/import` (scope `clients:write`) reads a delimited file and creates one client per valid row. The request is **`multipart/form-data`** — it carries a file, not a JSON body — with three fields: | Field | Type | Meaning | | --------- | ------- | --------------------------------------------------------------------------------- | | `file` | file | The CSV/XLSX/XLS/ODS/TXT file, up to **10 MB**. | | `mapping` | object | `{ "csv_header": "target_field" }`. Must map at least `name` and `tax_id`. | | `dry_run` | boolean | When `true`, validate and preview **without** creating anything. Default `false`. | The `mapping` tells the importer which spreadsheet column feeds which client field. The target set **must include `name` and `tax_id`** — without them a client cannot be created, and the request is rejected with `422` before any row is processed. ### Download the template [#download-the-template] `GET /v1/clients/import/template` streams a ready-to-fill CSV (UTF-8 with BOM so Excel opens it cleanly) whose header row lists every column the importer understands: `Nombre`, `NIF/CIF`, `Razón social`, `Email`, `Teléfono`, address fields, default VAT/retention, IBAN and more. Two example rows show the expected format. ```bash curl -s https://api.factuarea.com/v1/clients/import/template \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -o clients-template.csv ``` ### Dry run first, then import [#dry-run-first-then-import] Always validate with `dry_run=true` before you commit. The preview returns a per-row report and writes **nothing**: ```bash curl -s -X POST https://api.factuarea.com/v1/clients/import \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -F "file=@clients.csv" \ -F 'mapping={"Nombre":"name","NIF/CIF":"tax_id","Email":"email"};type=application/json' \ -F "dry_run=true" ``` ```json { "data": { "object": "client_import_preview", "total_rows": 3, "rows": [ { "row": 2, "status": "valid", "errors": [], "warnings": [] }, { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] }, { "row": 4, "status": "valid", "errors": [], "warnings": [] } ] } } ``` `row` is the **1-based line number in the file** (the header is row 1, so the first data row is row 2). `status` is `valid` or `error`; each `errors[]` item carries the offending `param`, a stable `code` and a Spanish `message`. When the preview is clean, resend the same file and mapping with `dry_run=false` (or omit it). Only valid rows are created; rejected rows come back in `failures[]`, and the response follows the partial-success shape with a per-row `results[]`: ```json { "data": { "total": 3, "successful": 2, "failed": 1, "failures": [ { "index": 1, "error_code": "invalid_tax_id", "error_message": "El NIF no es válido.", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] } ], "results": [ { "row": 3, "status": "error", "errors": [{ "param": "tax_id", "code": "invalid_tax_id", "message": "El NIF no es válido." }], "warnings": [] } ] } } ``` `total === successful + failed` always holds. A **duplicate** row (a client already on file, by the dedup rule) is **skipped**, not failed — it counts towards `successful` and is not created again, so re-running the same file is safe. <Callout type="info"> Branch on `error_code` / `code`, never on the message — the message is Spanish, human-facing text. The per-row codes come from the v1 error catalog. </Callout> ### File-size limit [#file-size-limit] The v1 import is **synchronous** so it can return the per-row result in the same response. Files are capped at **fewer than 200 rows**; a larger file is rejected with `422` and the `client_import_too_large` code. Split a big list into batches under that limit and import them in sequence. <Callout type="warn"> The `file` (10 MB) and `dry_run` rejections, and the 5000/200 caps, are enforced before any row is written. The dry-run preview is the cheapest way to catch malformed rows — use it before every real import. </Callout> --- # FACe invoicing (B2G) (/guides/face-invoicing) Invoicing a Spanish public administration (B2G) is mandatory through **FACe**, the general entry point for electronic invoices (Ley 25/2013). Factuarea generates the **FacturaE 3.2.2** XML for any issued invoice, signs it **XAdES-EPES** with your company certificate and presents it to the FACe web service — then keeps tracking the processing status reported by FACe until the invoice is paid (or rejected). The whole lifecycle is covered by the five operations of the **FacturaE** group in the API Reference: * [Download the FacturaE XML](/api-reference/facturae/public-api.v1.invoices.facturae) of an invoice — signed or unsigned, with or without FACe. * [Submit an invoice to FACe](/api-reference/facturae/public-api.v1.invoices.face_submissions.submit). * [List the submissions of an invoice](/api-reference/facturae/public-api.v1.invoices.face_submissions.list). * [Retrieve a submission](/api-reference/facturae/public-api.v1.face_submissions.show) to track its processing status. * [Request the cancellation](/api-reference/facturae/public-api.v1.face_submissions.cancel) of a submission. Reads use the `facturae:read` scope; submitting and cancelling require `facturae:write`. The FacturaE module is included in the **Empresario** and **Enterprise** plans. ## Before you submit [#prerequisites] <Steps> <Step> **Set the client's three DIR3 codes.** Every public-administration client carries three codes from the DIR3 directory, each matching `^[A-Z][A-Z0-9]{8,9}$` (e.g. `L01280796`). Set them when you create or update the client: ```bash curl -X PUT https://api.factuarea.com/v1/clients/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42 \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "dir3_accounting_office": "L01280796", "dir3_managing_body": "L01280796", "dir3_processing_unit": "L01280796" }' ``` | Field | DIR3 role | | ------------------------ | ----------------------- | | `dir3_accounting_office` | Oficina contable (01) | | `dir3_managing_body` | Órgano gestor (02) | | `dir3_processing_unit` | Unidad tramitadora (03) | The administration tells you the three codes (they often coincide); you can also look them up in the public DIR3 directory. </Step> <Step> **Upload an active signing certificate.** FACe only accepts **signed** invoices, so submission requires the FNMT (PKCS#12) certificate your company already uses for VeriFactu (`POST /v1/verifactu/certificates`). Without an active certificate the submission fails with `signing_certificate_required`. </Step> <Step> **Issue the invoice.** Draft invoices cannot travel to FACe — sending or emitting the invoice first is what freezes its legal content. Drafts answer `invoice_not_emittable_for_facturae`. </Step> </Steps> ## Downloading the FacturaE XML [#download] You can download the XML at any time — for manual presentation, archiving or validation — without involving FACe: ```bash curl -OJ https://api.factuarea.com/v1/invoices/0197b1c2-89ab-7def-8123-456789abcdef/facturae \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` With an active certificate the body is signed XAdES-EPES (Facturae v3.1 signature policy) and the file is named `.xsig`; without one, the XML comes back unsigned as `.xml`. The `X-Facturae-Signed: true|false` response header tells both cases apart. See the [endpoint reference](/api-reference/facturae/public-api.v1.invoices.facturae). <Callout type="info"> Downloading tolerates a missing certificate (you get unsigned XML); **submitting to FACe does not** — FACe requires the signature. </Callout> ## Submitting to FACe [#submit] The [submit operation](/api-reference/facturae/public-api.v1.invoices.face_submissions.submit) takes no request body: the invoice travels in the path and the DIR3 codes are read from the client at submission time (and snapshotted on the submission): ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197b1c2-89ab-7def-8123-456789abcdef/face-submissions \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Idempotency-Key: $(uuidgen)" ``` Response (`201`): ```json { "data": { "id": "0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "object": "face_submission", "invoice_id": "0197b1c2-89ab-7def-8123-456789abcdef", "status": "submitted", "registry_number": "202612345678", "dir3_accounting_office": "L01280796", "dir3_managing_body": "L01280796", "dir3_processing_unit": "L01280796", "error_code": null, "error_message": null, "status_updated_at": "2026-06-12T10:15:00Z", "last_polled_at": null, "created_at": "2026-06-12T10:15:00Z" } } ``` `registry_number` is the FACe registry entry that proves the presentation — keep it for any dispute with the administration. ## Tracking the processing status [#states] FACe reports how the administration processes the invoice. Factuarea polls FACe periodically and updates each submission — retrieving the [submission detail](/api-reference/facturae/public-api.v1.face_submissions.show) (or the invoice's [submission history](/api-reference/facturae/public-api.v1.invoices.face_submissions.list)) is the way to track progress; there is no refresh endpoint in v1: ```bash curl https://api.factuarea.com/v1/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` | `status` | Meaning | | ------------------------ | ---------------------------------------------------------------------------------------- | | `submitted` | Presented to FACe; registry number assigned. | | `registered_rcf` | Registered in the RCF (the administration's accounting registry of invoices). | | `accounted` | Recognized as an accounting obligation by the administration. | | `paid` | The administration reports the invoice as paid. | | `rejected` | Rejected by the administration — check the reason in FACe and issue a corrected invoice. | | `cancellation_requested` | You requested cancellation; awaiting FACe confirmation. | | `cancelled` | Cancellation confirmed by FACe. | | `error` | Local transmission error — `error_code` and `error_message` carry the detail. | Prefer push over polling? Subscribe to the [webhook events](/guides/webhooks) `facturae.face_submitted`, `facturae.face_status_changed` and `facturae.face_cancellation_requested`. ## Requesting cancellation [#cancel] While the invoice has not been paid or rejected you can [request its cancellation](/api-reference/facturae/public-api.v1.face_submissions.cancel) (anulación 4200). The `reason` is mandatory and travels to FACe: ```bash curl -X POST https://api.factuarea.com/v1/face-submissions/0197c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d/cancel \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "reason": "Factura emitida por error al organismo equivocado." }' ``` Cancellation is only allowed in a cancellable state (`submitted`, `registered_rcf`, `accounted`); otherwise the call answers `face_submission_not_cancellable`. The submission moves to `cancellation_requested` until FACe confirms the final `cancelled` state. ## Test mode [#sandbox] With a test key (`fact_test_`) the whole flow is **simulated**: no SOAP call ever reaches FACe and the submission gets a synthetic registry number prefixed `FACE-SANDBOX-*`. Signature and DIR3 validations still apply, so the sandbox exercises the same error paths as production. See [test mode](/guides/test-mode). ## Errors [#errors] | HTTP | `code` / `subcode` | When | | ---- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 404 | `resource_not_found` | The invoice or submission doesn't exist or belongs to another company. | | 422 | `business_rule_violation` / `invoice_not_emittable_for_facturae` | The invoice is a draft — issue it first. | | 422 | `business_rule_violation` / `client_missing_dir3_codes` | The client lacks one or more DIR3 codes. | | 422 | `business_rule_violation` / `signing_certificate_required` | No active signing certificate — upload one via `POST /v1/verifactu/certificates`. | | 422 | `business_rule_violation` / `face_submission_not_cancellable` | The submission is not in a cancellable state. | | 409 | `resource_already_exists` / `face_submission_already_exists` | An active submission already exists for the invoice. | | 403 | `insufficient_scope` | The key lacks the `facturae:write` scope. | | 502 | `face_transmission_failed` | The FACe web service is down — nothing is persisted; retry later. | --- # Fiscal cookbook (/guides/fiscal-cookbook) Every recipe below is a complete sequence of calls, with the `factuarea` CLI equivalent, and a link to the guide that explains **why** it is done that way. The guides carry the fiscal reasoning; this page carries the order of operations. <Callout type="info"> The CLI command tree is generated from the OpenAPI document, so every endpoint is reachable either as a named command or through the generic escape hatch `factuarea api <method> <path>`. The recipes use the escape hatch wherever the named form would be guesswork; both hit the same v1 endpoint. See [CLI usage](/cli/usage). </Callout> Set your key once: ```bash export FACTUAREA_KEY="fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ## How this page relates to the other four questions [#dimensions] Each fiscal guide answers four questions about its scenario. This page is a recipe book, so it answers them by delegation, and says so rather than omitting the sections. ### When each recipe applies [#when] Stated at the top of each recipe as its **goal**. The preconditions — which invoice status admits which operation, which document types are eligible — belong to the linked guide and are not restated here. ### What the API sends [#api] This is the only dimension the page covers in full: every recipe shows the complete request and its CLI equivalent, with real field names from the v1 contract. ### What appears on the PDF [#pdf] **Not covered here.** No recipe changes the printed document beyond what its guide already describes — the legal QR block, the disbursement rows in the totals block, the corrective's own numbering. See [Disbursements](/guides/disbursements#pdf) and [Corrective invoices](/guides/corrective-invoices#pdf). ### What reaches the AEAT [#aeat] **Not covered here.** The declarations produced by these sequences are described in [VeriFactu submission states](/guides/verifactu-submission-states#aeat) and, per scenario, in each linked guide. Recipe 1 is the only one whose *purpose* is to observe the declaration, and it does so by reading the billing record. ## 1 · Issue an invoice and wait for AEAT acceptance [#issue-and-wait] **Goal:** create, issue, and confirm the tax authority registered it. <Steps> <Step> **Create and issue in one call.** `options.issue_directly` saves the separate send step, and the two events it fires cannot produce a duplicate registration — the command is idempotent per invoice. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Servicio de consultoría", "quantity": 2, "unit_price": 150, "tax_rate": 21, "regime_key": "01" } ], "options": { "issue_directly": true } }' ``` ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","issued_on":"2026-06-01","due_on":"2026-07-01","lines":[{"description":"Servicio de consultoría","quantity":2,"unit_price":150,"tax_rate":21,"regime_key":"01"}],"options":{"issue_directly":true}}' ``` </Step> <Step> **Poll the billing record** until it leaves the non-final states. Read `status` and, once accepted, `aeat_csv` — that is the value you reconcile against the tax authority. ```bash curl https://api.factuarea.com/v1/invoices/{invoice_id}/verifactu \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` ```bash factuarea api get /v1/invoices/{invoice_id}/verifactu --json ``` </Step> <Step> **Or stop polling.** Subscribe to the invoice VeriFactu webhook events instead and react when the outcome arrives. See [Webhooks](/guides/webhooks). </Step> </Steps> Fundamentals: [VeriFactu auto-submission](/guides/verifactu-auto-submission) for the gates that decide whether a record is created at all, and [VeriFactu submission states](/guides/verifactu-submission-states) for what each state means. ## 2 · Correct an amount error [#correct-amount] **Goal:** an issued invoice charged too much. Reduce it without annulling. A downward correction is a corrective **by differences**, with negative amounts. `correction_type: "partial"` yields that nature; a substitution could not carry a negative base. ```bash curl -X POST https://api.factuarea.com/v1/invoices/{invoice_id}/corrective \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "correction_reason": "error_importe", "correction_type": "partial", "lines": [ { "description": "Ajuste por error de importe", "quantity": -1, "unit_price": 200, "tax_rate": 21 } ] }' ``` ```bash factuarea api post /v1/invoices/{invoice_id}/corrective -d '{"correction_reason":"error_importe","correction_type":"partial","lines":[{"description":"Ajuste por error de importe","quantity":-1,"unit_price":200,"tax_rate":21}]}' ``` Answer: `201` with the new corrective invoice and a `Location` header. List every corrective issued against the original with `GET /v1/invoices/{id}/correctives`. Fundamentals: [Corrective invoices](/guides/corrective-invoices). If the invoice is still unpaid and the whole document is wrong rather than one amount, check [Annul or correct](/guides/annul-vs-correct) first — annulment may be the right operation. ## 3 · Substitute simplified invoices with a complete one [#substitute] **Goal:** a customer who collected several tickets now needs one deductible invoice. One call. You pass the recipient and the simplified invoices to aggregate, and get back a complete substitute invoice, already issued: ```bash curl -X POST https://api.factuarea.com/v1/invoices/substitute-simplified \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "simplified_invoice_ids": [ "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f601", "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f602" ], "notes": "Consumos de junio" }' ``` ```bash factuarea api post /v1/invoices/substitute-simplified -d '{"client_id":"…","simplified_invoice_ids":["…","…"],"notes":"Consumos de junio"}' ``` The originals are not annulled: they keep their fiscal status and record that they have been substituted. Fundamentals: [Simplified or full invoices](/guides/simplified-vs-full-invoices). ## 4 · Pass on a disbursement [#disbursement] **Goal:** invoice your fee plus a duty you paid on the customer's behalf, without the duty entering your taxable base. The disbursement line carries **no tax of its own** and **must** carry the origin reference. At least one ordinary line is required alongside it. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Honorarios de constitución de sociedad", "quantity": 1, "unit_price": 1000, "tax_rate": 21 }, { "description": "Tasa del Registro Mercantil", "quantity": 1, "unit_price": 150, "line_type": "SUPLIDO", "source_invoice_reference": "RM-2026-0451" } ] }' ``` ```bash factuarea invoices create -d '{"client_id":"…","series_id":"…","issued_on":"2026-06-01","due_on":"2026-07-01","lines":[{"description":"Honorarios","quantity":1,"unit_price":1000,"tax_rate":21},{"description":"Tasa del Registro Mercantil","quantity":1,"unit_price":150,"line_type":"SUPLIDO","source_invoice_reference":"RM-2026-0451"}]}' ``` Check the response: `total` is 1210, `total_disbursements` is 150 and `total_to_pay` is 1360. Charge and reconcile against `total_to_pay`, not `total`. Fundamentals: [Disbursements](/guides/disbursements). ## 5 · Invoice a customer outside the EU [#export] **Goal:** an export, exempt under art. 21 LIVA. <Steps> <Step> **Create the customer with an alternative identification.** The type must be legal for the country — an intra-community VAT number is not. ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Inc", "alternative_id": { "type": "passport", "value": "X1234567", "country_code": "US" } }' ``` </Step> <Step> **Issue with the exemption declared per line.** The header regime is read-only over the public API, so the exemption is stated on the line: ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "{client_id}", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "notes": "Operación exenta por exportación (art. 21 LIVA)", "lines": [ { "description": "Suministro de equipos", "quantity": 1, "unit_price": 4000, "tax_rate": 0, "exemption_reason": "E2", "regime_key": "02" } ] }' ``` </Step> </Steps> Fundamentals: [International customers](/guides/international-customers) — and read its note on reverse charge before assuming the same shape works for services. ## 6 · Repair a record the AEAT rejected [#repair] **Goal:** the tax authority refused the declaration because of a data error. Fix it without annulling the invoice. <Steps> <Step> **Confirm it is a rejection, not a technical failure.** A `rejected` status means the AEAT read the declaration; `error` means it never arrived and is retried automatically. ```bash curl "https://api.factuarea.com/v1/verifactu/records?status=rejected" \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` </Step> <Step> **Fix the data at its source.** The declaration is regenerated from the invoice and the *current* master data — correct the customer's tax ID or registered name and the new values are picked up. ```bash curl -X PUT https://api.factuarea.com/v1/clients/{client_id} \ -H "Authorization: Bearer $FACTUAREA_KEY" \ -H "Content-Type: application/json" \ -d '{"tax_id": "B12345678"}' ``` </Step> <Step> **Resubmit.** No request body: the content is regenerated server-side. ```bash curl -X POST https://api.factuarea.com/v1/verifactu/records/{record_id}/subsanar \ -H "Authorization: Bearer $FACTUAREA_KEY" ``` ```bash factuarea api post /v1/verifactu/records/{record_id}/subsanar --json ``` </Step> <Step> **Watch the outcome.** The record is transmitted again and ends accepted — or rejected once more if the data is still wrong, in which case you can repeat. There is no attempt limit on this path. </Step> </Steps> If the answer is `422` telling you an annulment is required, the correction touches a fingerprint field — the total, the number, the date, the issuer tax ID or the invoice type — and the record cannot be repaired in place. Fundamentals: [VeriFactu record subsanación](/guides/verifactu-subsanacion) for the full error table, and [VeriFactu submission states](/guides/verifactu-submission-states#retry-vs-subsanar) for retry versus subsanación. ## Traceability [#traceability] This page states no fiscal rule of its own: it sequences calls whose grounds are established elsewhere. Each recipe **inherits** the traceability of the guide it links: | Recipe | Inherits from | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Issue and wait | [VeriFactu auto-submission](/guides/verifactu-auto-submission#traceability) · [VeriFactu submission states](/guides/verifactu-submission-states#traceability) | | Correct an amount | [Corrective invoices](/guides/corrective-invoices#traceability) · [Annul or correct](/guides/annul-vs-correct#traceability) | | Substitute simplified | [Simplified or full invoices](/guides/simplified-vs-full-invoices#traceability) | | Pass on a disbursement | [Disbursements](/guides/disbursements#traceability) | | Invoice outside the EU | [International customers](/guides/international-customers#traceability) · [Line tax classification and exemptions](/guides/line-tax-classification-and-exemptions#traceability) | | Repair a rejected record | [VeriFactu submission states](/guides/verifactu-submission-states#traceability) | --- # Fiscal invoice examples (/guides/fiscal-invoice-examples) Spanish invoicing covers many tax scenarios — domestic B2B, intra-EU goods and services, OSS distance sales, IGIC in the Canary Islands, IPSI in Ceuta/Melilla, IRPF withholding, equivalence surcharge, exempt and non-subject operations. Getting the right combination of `tax_rate`, `exemption_reason`, `regime_key`, `retention_rate` and `surcharge_rate` on each line is the hard part. To make this concrete, the API Reference ships **four named, ready-to-send request examples** on [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) — a curated, representative set rather than one per scenario. Each one is a valid payload you can copy, adapt and send: pick the closest one in the request-body examples dropdown, then use the table below and the guide linked on each row for the rest. ## The 21 scenarios [#scenarios] Every row below is a scenario you can express line by line with the fields above. The four marked **★** are the ones that also ship as named request examples you can pick straight from the dropdown; the remaining seventeen are documented here and in the linked guide, but have no named example in the spec — build them from the closest starred one. | Scenario key | Scenario | Guide | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | ★ `b2b_nacional` | Domestic B2B, 21% VAT per line (general AEAT regime 01). | [Regime keys](/guides/regime-keys) | | `b2b_nacional_iva_reducido` | Reduced (10%) or super-reduced (4%) VAT, regime 01. | [Line tax classification](/guides/line-tax-classification-and-exemptions) | | ★ `b2c` | Final consumer (no recipient tax ID; simplified invoice where applicable). | [Simplified or full](/guides/simplified-vs-full-invoices) | | ★ `intracomunitario_bienes` | Intra-EU supply of goods, exempt `E5` (art. 25 LIVA). | [International customers](/guides/international-customers) | | `intracomunitario_servicios` | Intra-EU B2B services, reverse charge — qualification `S2` (subject, **not** exempt, charged quota `0`) derived from the `isp` header regime, not an exemption cause. | [International customers](/guides/international-customers) | | `oss` | OSS distance sales (destination-country VAT), `regime_key: 17`. | [International customers](/guides/international-customers) · [Regime keys](/guides/regime-keys) | | `igic_canarias` | IGIC in the Canary Islands, `regime_key: 08`. | [Territorial taxes](/guides/territorial-taxes) | | `ipsi_ceuta_melilla` | IPSI in Ceuta / Melilla, `regime_key: 08`. | [Territorial taxes](/guides/territorial-taxes) | | ★ `con_irpf` | Per-line IRPF withholding (`retention_rate`). | [Line tax classification](/guides/line-tax-classification-and-exemptions) | | `con_recargo_equivalencia` | Equivalence surcharge using a legal VAT↔surcharge pair, `regime_key: 18`. | [Line tax classification](/guides/line-tax-classification-and-exemptions) · [Regime keys](/guides/regime-keys) | | `exenta_articulo_20` | Exempt under art. 20 LIVA, `exemption_reason: E1`. | [Line tax classification](/guides/line-tax-classification-and-exemptions) | | `exenta_exportacion` | Export outside the EU, exempt `E2` (art. 21), `regime_key: 02`. | [International customers](/guides/international-customers) · [Regime keys](/guides/regime-keys) | | `no_sujeta` | Non-subject operation, `exemption_reason: N1` / `N2`. | [Line tax classification](/guides/line-tax-classification-and-exemptions) | | `inversion_sujeto_pasivo_nacional` | Domestic reverse charge (e.g. construction work), `tax_rate: 0`. | [Line tax classification](/guides/line-tax-classification-and-exemptions) | | `regimen_especial_bienes_usados` | Used-goods margin scheme (REBU), `regime_key: 03`. | [Regime keys](/guides/regime-keys) | | `regimen_agencias_viajes` | Travel-agency scheme (REAV), `regime_key: 05`. | [Regime keys](/guides/regime-keys) | | `criterio_caja` | Cash-basis scheme, `regime_key: 07`. | [Regime keys](/guides/regime-keys) | | `multilinea_iva_mixto` | Several lines at different VAT rates (21% / 10% / 4%). | [Line tax classification](/guides/line-tax-classification-and-exemptions) | | `con_descuento_y_metadata` | Per-line `discount_percent` plus integration `metadata`. | [Fiscal cookbook](/guides/fiscal-cookbook) | | `con_idempotency_key` | Safe retries with the `Idempotency-Key` header. | [Fiscal cookbook](/guides/fiscal-cookbook) | | `cliente_extranjero_alternative_id` | Foreign recipient with an alternative ID (AEAT type↔country matrix). | [International customers](/guides/international-customers) | Each of the four starred examples is also published as a reusable `components.examples.invoice_*` entry in the OpenAPI spec, so SDKs and tooling can resolve them by `$ref`. <Callout type="info"> The fiscal value objects (regime, exemption reason, IRPF, surcharge) come from Factuarea's tax engine. The examples show valid combinations; for the field-by-field contract see the [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) request schema and [Amounts and dates](/guides/amounts-and-dates). </Callout> ## Corrective invoices by R-code [#r-codes] A corrective (rectificativa) invoice carries the AEAT correction code that states **why** the original is being corrected. [`POST /v1/invoices/{id}/corrective`](/api-reference/invoices/public-api.v1.invoices.corrective) ships one named example per code, each a valid payload that produces that exact `correction_code`: | Example | Code | Applies to | Guide | | ------------------ | ---- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `r1_error_fundado` | `R1` | Well-founded error in law / cancellation. Full invoices F1/F3. | [Corrective invoices](/guides/corrective-invoices) | | `r2_concurso` | `R2` | Insolvency proceedings of the recipient. F1/F3. | [Corrective invoices](/guides/corrective-invoices) | | `r3_incobrable` | `R3` | Bad debts. F1/F3. | [Corrective invoices](/guides/corrective-invoices) | | `r4_otras` | `R4` | Other causes; full or partial (`correction_type: partial` with `lines`). F1/F3. | [Corrective invoices](/guides/corrective-invoices) | | `r5_simplificada` | `R5` | Correction of a **simplified** invoice. F2 only. | [Corrective invoices](/guides/corrective-invoices) · [Simplified or full](/guides/simplified-vs-full-invoices) | Pass `correction_code` explicitly to select the R-code; `R5` applies only to simplified (F2) invoices. <Callout type="warn"> A corrective is itself a fiscal document: once issued it is reported to AEAT via VeriFactu just like any other invoice. Use the example that matches the legal reason — the code is not cosmetic. </Callout> --- # Glossary (/guides/glossary) The Factuarea API models Spanish invoicing and tax-compliance concepts. If you are integrating from outside Spain — or just want a precise reference — this glossary explains the domain terms that appear in field names, enum values and error messages, and how each maps to the API. <Callout type="info"> API error messages (`error.message`) are returned **in Spanish** because they mirror the real API response. The `type`, `code` and `subcode` fields are stable English identifiers — match on those, not on the message text. See [Errors](/guides/errors). </Callout> ## Fiscal identifiers [#fiscal-identifiers] | Term | Definition | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **NIF / CIF / NIE** | The Spanish fiscal tax number. *NIF* (Número de Identificación Fiscal) identifies residents and companies, *CIF* was the legacy code for legal entities, and *NIE* (Número de Identidad de Extranjero) identifies foreign residents. In the API they all live in the single `tax_id` field on `clients`, `suppliers` and your account. For non-Spanish counterparties use `alternative_id` instead — it is mutually exclusive with `tax_id`. | | **VAT ID (NIF intracomunitario)** | An EU intra-community VAT number, exposed as the `vat_id` field on `clients` and `suppliers`. Distinct from `tax_id`: it identifies the party for VAT-exempt intra-EU operations, not for domestic fiscal purposes. | | **AEAT** | Agencia Estatal de Administración Tributaria — the Spanish tax agency. It is the recipient of VeriFactu records, the authority behind [Modelo](#tax-declarations) declarations, and the issuer of the [CSV](#verifactu-records--hash-chain). All `aeat_*` fields and the `/v1/verifactu/aeat-access/*` endpoints relate to it. | ## Taxes [#taxes] | Term | Definition | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **IVA (VAT)** | Impuesto sobre el Valor Añadido — Spanish value-added tax. In the API it is a tax of `type: "vat"` in the tax catalog. Apply it per line via `tax_rate_id`; totals are computed by the API (`subtotal + total_vat + total_surcharge − total_retention`). See the Taxes section in the API Reference. | | **Retención (IRPF withholding)** | A withholding deducted from a line and remitted to the AEAT on the recipient's behalf, typically IRPF (Impuesto sobre la Renta de las Personas Físicas) for freelancers. Modeled as a tax of `type: "retention"`. It **subtracts** from the document total, unlike VAT and surcharge. | | **Recargo de equivalencia (equivalence surcharge)** | A special VAT regime for retailers: an extra surcharge added on top of VAT so the retailer does not file VAT returns separately. Modeled as a tax of `type: "surcharge"`; a counterparty subject to it carries `is_surcharge_subject: true`. It **adds** to the document total. | ## Documents [#documents] | Term | Definition | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Serie (numbering series)** | The sequential, gap-free numbering sequence an invoice belongs to (`series_id`). A series is **immutable per AEAT compliance** — once created it cannot be edited (the `PUT` method returns `405`). Test mode uses the sandbox company's own series and never touches your production numbering. See the Series section in the API Reference and [Test mode](/guides/test-mode). | | **Rectificativa (corrective invoice)** | A corrective invoice that amends a previously issued one — the legal way to fix an invoice, since issued invoices cannot be edited or deleted. Created via `POST /v1/invoices/{id}/corrective`; the result is a **new** invoice with `is_corrective: true` and a `corrective` object, mapped to an AEAT `R1`–`R5` type code. The code is derived from the `correction_reason` slug by default, but you can **force it explicitly** with `correction_code` (`R1`–`R5`): a simplified (`F2`) original admits only `R5`, a complete (`F1`/`F3`) original only `R1`–`R4` — an incompatible code returns `422` with the legal codes in `error.allowed_values`. An optional `justification` (`min:10`) records the documentary trace the LIVA requires for some causes (insolvency, bad debt). Compare with **annul** (`POST /v1/invoices/{id}/annul`), which voids without amending. | | **Factura simplificada (simplified invoice)** | A reduced-data invoice (AEAT type `F2`) allowed for small amounts under Real Decreto 1619/2012 art. 4, with no full recipient details. Check eligibility with `POST /v1/invoices/simplified-eligibility`; group several into one full substitutive invoice (type `F3`) with `POST /v1/invoices/substitute-simplified`. A full ordinary invoice is type `F1`. | | **Proforma** | A non-fiscal preview invoice used to quote or request payment before issuing the real (fiscal) invoice. It carries no legal numbering and can be converted to an invoice via `POST /v1/proformas/{id}/convert`. Lifecycle: `draft`, `accepted`, `rejected`, `cancelled`, `expired`, `converted`. | | **Albarán (delivery note)** | A document tracking goods delivered to a customer (the `delivery_notes` resource), which can later be converted to an invoice. Supports a handwritten recipient signature (base64 PNG). Public lifecycle: `draft`, `sent`, `signed`, `invoiced`, `cancelled`. | | **`external_id` (integration key)** | An external business identifier — the record's ID in your own ERP/CRM/e-commerce — stored on a resource to map and deduplicate it across integrations. Free-format (≤ 100 chars), unique per company, and orthogonal to Factuarea's own identifiers (`id`, `number`, `sku`). Look a record up by it with `POST /v1/{resource}/find-by-external-id` (body `{ "external_id": "..." }`). Ideal as the mapping key when migrating from another platform — see [Migrate from Holded](/guides/migration-from-holded). | ## VeriFactu & AEAT compliance [#verifactu--aeat-compliance] | Term | Definition | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **VeriFactu** | The Spanish anti-fraud invoicing system (SIF) under which each issued invoice generates a tamper-evident "Alta" record submitted to the AEAT. In `live` the record is transmitted to the AEAT; in `test` it is created locally but **never transmitted**. Managed under the `/v1/verifactu/*` endpoints. See [Test mode](/guides/test-mode). | | **Huella (hash chain)** | The chained SHA-256 fingerprint of a VeriFactu record (`huella` field) that links each record to the previous one, making the sequence tamper-evident. Look up a record by it with `POST /v1/verifactu/records/find-by-huella`, and verify the whole chain's integrity with `GET /v1/verifactu/chain/validate`. | | **CSV (Código Seguro de Verificación)** | The **Secure Verification Code** the AEAT returns when it accepts a VeriFactu record (the `aeat_csv` field; `null` until assigned). This is an AEAT receipt code — **not** a comma-separated-values file. Look up a record by it with `POST /v1/verifactu/records/find-by-csv`. | | **FacturaE** | The Spanish electronic-invoice XML format (FacturaE 3.2.2) required for B2G invoicing to public administration. Download it for an invoice with `GET /v1/invoices/{id}/facturae` (signed XAdES-EPES when a certificate is active) and submit it to FACe via `/v1/face-submissions`. See [FACe invoicing](/guides/face-invoicing). | | **FACe** | The general entry point of the Spanish public administration for electronic invoices (Ley 25/2013). Factuarea presents the signed FacturaE XML to the FACe web service and tracks the processing status (`submitted` → `registered_rcf` → `accounted` → `paid`). See [FACe invoicing](/guides/face-invoicing). | | **DIR3** | The Spanish directory of public-administration units. Every B2G client carries three DIR3 codes — oficina contable (01), órgano gestor (02) and unidad tramitadora (03) — required by FACe, with format `^[A-Z][A-Z0-9]{8,9}$`. | | **Declaración responsable** | A formal compliance declaration (declaración responsable) that the SIF software producer — Factuarea — issues to attest VeriFactu conformity. It is producer-level and read-only (not per-company): retrieve the current one with `GET /v1/verifactu/declaracion-responsable`. | ## Tax declarations [#tax-declarations] | Term | Definition | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Modelo 303** | The quarterly Spanish VAT (IVA) self-assessment return filed with the AEAT. Generate it with `POST /v1/tax_reports/303`, indicating the quarter (`1`–`4`). The response includes a per-VAT-rate breakdown (`{base, cuota}` in cents). See the Tax reports section in the API Reference. | | **Modelo 347** | The yearly informational return declaring third parties with whom annual operations exceeded the legal threshold. Generate it with `POST /v1/tax_reports/347`; it is annual and does **not** accept a quarter (sending one returns a validation error). | ## Time tracking (control horario) [#time-tracking] The [time-tracking system](/guides/workforce-overview) covers the Spanish working-time duty. Its terms appear in field names and enum values across the workforce domains, all gated behind the `control_horario` module. | Term | Definition | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **RD-ley 8/2019** | Royal Decree-law 8/2019 (art. 34.9 of the Workers' Statute), which obliges Spanish employers to keep an objective, reliable and unalterable daily record of every employee's working day and retain it for four years for the Labour Inspectorate (ITSS). Factuarea builds it as an append-only ledger sealed by a per-company SHA-256 hash chain — the VeriFactu tamper-evidence pattern applied to attendance. See [Time tracking](/guides/workforce-overview). | | **Fichaje (time entry)** | Each clock event — clock in, pause, resume, clock out — appended to the immutable ledger (the `time_entries` resource) and never edited or deleted. The live session state (`working`, `paused`, `finished`) is derived from the ledger, not stored in a column. See [Time clock](/guides/time-clock). | | **Jornada (working day)** | An employee's working day. It may split into several shifts (jornada partida) when the employee clocks out and back in on the same day; the expected weekly hours come from the assigned work schedule. | | **Registro inalterable (ledger)** | The append-only, hash-chained time record. There is no update or delete: a mistake is fixed by a correction request that appends a new entry referencing the original, so both the error and its fix stay in the record. Verify its integrity with `GET /v1/time-entries/chain/validate`. | | **Cierre mensual (monthly close)** | A snapshot that freezes a finished month's balances and absence breakdown and locks the period against retroactive entries (the `monthly-register-closes` resource). It moves `closed ⇄ reopened`; reopening is an audited recovery. See [Monthly close](/guides/monthly-time-close). | | **Sellado (seal)** | The optional, irreversible signature of a monthly close: a canonical SHA-256 digest plus a detached RSA-SHA256 signature made with the company certificate, so an auditor can prove the snapshot has not changed since it was signed. One seal per close — re-sealing returns `409`. | | **Asiento de empleado (employee seat)** | The billing unit for time tracking. Employees are billed through a dedicated monthly `employee-seats` add-on whose quantity follows the active roster; an employee never counts against the plan `users` limit. See [Employee seat billing](/guides/employee-seats). | | **Tipo de ausencia (absence type)** | What an employee can request — holiday, sick leave, a personal day — carrying whether it is paid, whether it needs approval, and a measurement unit (`days` or `hours`). A default Spanish set is seeded into every new company. See [Absences](/guides/absences). | | **Política de ausencia (absence policy)** | The rule that decides how much and for whom: an allowance (`limited` days or `unlimited`), an accrual method (`annual` or `monthly`), the types it covers and the employees it is assigned to. | | **Saldo (balance)** | The remaining allowance per employee and absence type, derived from the policy accrual minus approved requests (the `absence-balances` resource). | | **Presencialidad (presence)** | The read-only view of who is working right now and who is in office or remote today, derived from the ledger, the schedules and the roster — never persisted. There is no `presence:write` scope: declaring office/remote presence is a portal-only task. See [Presence](/guides/presence). | --- # Idempotency (/guides/idempotency) Write operations (`POST`, `PATCH`, `DELETE`) can be received multiple times if the connection drops mid-response, your integration retries after a timeout, or there are automatic retries in an intermediate gateway. To prevent the same POST from creating two invoices, the API supports the `Idempotency-Key` header. ## How it works [#how-it-works] 1. The client generates a unique key per operation (a UUID v7 is the recommended choice, for consistency with the API identifiers). 2. Send it as a header on the first request: ```http POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b ``` 3. The API stores the result (status code, headers and body) associated with that key for **24 hours**. 4. If a new request arrives with the same key within the TTL, the API returns the cached response without re-executing the handler. The response returned on a replay includes the `Idempotent-Replayed: true` header so you can distinguish it. ## Key format [#key-format] * An **opaque string** to the server: any unique value is valid (UUID v7, UUID v4, ULID, nanoid, etc.). * Length between 1 and 255 characters. * Recommendation: UUID v7 (`Str::uuid7()`, or any UUID v7 generator), for consistency with the API identifiers. ```bash KEY=$(uuidgen) curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` ## Automatic with the official SDKs [#automatic-with-the-official-sdks] The [TypeScript and PHP SDKs](/sdks) attach an `Idempotency-Key` to every mutation automatically and **reuse the same key across the retries of one call**, so a retried request never double-creates. Override it per call when you want app-level deduplication: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts // auto-generated key await factuarea.invoices.create(body); // pin your own key (e.g. your order id) await factuarea.invoices.create(body, { idempotencyKey: "order-4711" }); ``` </Tab> <Tab value="PHP"> ```php // auto-generated key $factuarea->invoices->publicApiV1InvoicesCreate($body); // pin your own key $factuarea->invoices->publicApiV1InvoicesCreate($body, idempotencyKey: 'order-4711'); ``` </Tab> </Tabs> ## Payload fingerprint [#payload-fingerprint] The key is bound not only to the `Idempotency-Key` but also to a **fingerprint** of the request: ``` fingerprint = sha256(method + " " + path + "\n" + canonicalize(body)) ``` Where `canonicalize(body)` is JSON with keys sorted alphabetically. If you replay the same key with a **different payload**, the API responds **409 Conflict**: ```json { "error": { "type": "idempotency_error", "code": "idempotency_key_reused", "message": "This Idempotency-Key was previously used with a different request body.", "request_id": "req_..." } } ``` This is protection against bugs: no reasonable caller changes the body while keeping the same key. If you need to retry with different data — use a new key. ## TTL [#ttl] Entries are persisted in the `idempotency_keys` table for **86,400 seconds (24 h)**. After that they are purged by a daily schedule. If you reuse a key outside the window it's treated as a new one. ## external\_id vs Idempotency-Key [#external_id-vs-idempotency-key] Both protect you from duplicates, but they solve different problems — and you can use them together. | | `Idempotency-Key` | `external_id` | | ---------- | ----------------------------------------------- | ---------------------------------------------------------- | | What it is | A header on a single `POST`. | A business key stored **on the resource**. | | Lifetime | **Ephemeral** — 24h window, then purged. | **Durable** — permanent, never expires. | | Scope | Deduplicates **transport retries** of one call. | Deduplicates by your own **integration key** (ERP/CRM id). | | Queryable | No. | **Yes** — `POST /v1/{resource}/find-by-external-id`. | Use the **`Idempotency-Key`** to make a retry safe: if the network drops mid-response, replaying the same key within 24h returns the cached result instead of creating a second invoice. It is about the *delivery* of one request. Use **`external_id`** to tie a Factuarea resource to a record in your own system (an order id, an ERP document number). Send it in the create body and the API enforces it is unique per company (`UNIQUE(company_id, external_id)`). Later you can look the resource up by that key, without storing Factuarea's `id`: ```bash curl -s -X POST https://api.factuarea.com/v1/invoices/find-by-external-id \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "ORDER-4711" }' | jq '.data.id' ``` In short: `Idempotency-Key` is a short-lived retry guard; `external_id` is your permanent, queryable link. A typical integration sets **both** — a fresh key per attempt, and a stable `external_id` per business object. ## Per-endpoint recommendations [#per-endpoint-recommendations] | Endpoint | Idempotency recommended | | ------------------------------------------ | ---------------------------------------------- | | `POST /v1/invoices` | **Yes** (critical) | | `POST /v1/quotes` | **Yes** | | `POST /v1/clients` | **Yes** | | `POST /v1/invoices/{id}/send` | Yes | | `POST /v1/invoices/{id}/mark-paid` | Yes | | `POST /v1/invoices/{id}/payments` | **Yes** (a retried payment would double-count) | | `POST /v1/purchase_invoices/{id}/payments` | **Yes** (a retried payment would double-count) | | `GET /v1/...` | N/A (no effect) | | `PATCH /v1/...` | Optional (PATCH is idempotent by definition) | | `DELETE /v1/...` | Optional | Stripe documents the same pattern — if you come from there, the contract is identical. ## Safe retry example [#safe-retry-example] <Tabs items="['Python (tenacity)', 'Node.js']"> <Tab value="Python (tenacity)"> ```python import os, uuid, requests from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type @retry( stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, max=10), retry=retry_if_exception_type(requests.exceptions.RequestException), ) def create_invoice(payload): key = str(uuid.uuid4()) return requests.post( 'https://api.factuarea.com/v1/invoices', json=payload, headers={ 'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}", 'Idempotency-Key': key, }, timeout=30, ) ``` </Tab> <Tab value="Node.js"> ```javascript async function createInvoiceWithRetry(payload, maxAttempts = 5) { const key = crypto.randomUUID(); let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const res = await fetch('https://api.factuarea.com/v1/invoices', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Idempotency-Key': key, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (res.ok) return res.json(); if (res.status >= 500) { await new Promise(r => setTimeout(r, 2 ** attempt * 100)); continue; } return res.json(); } catch (err) { lastError = err; await new Promise(r => setTimeout(r, 2 ** attempt * 100)); } } throw lastError; } ``` </Tab> </Tabs> <Callout type="warn"> **Important**: the key must remain **constant across retries of the same POST**. If you generate a new key on each retry you lose the protection. In the Python `tenacity` example, the `key` is generated outside the closure and reused across all retries. </Callout> ## What idempotency is NOT [#what-idempotency-is-not] * **Not** the same as rate limiting: an idempotent key replayed within the TTL **doesn't count** against your quota; but different keys with the same payload do count, one by one. * **Not** a substitute for a distributed lock on your side. If two workers concurrently create invoices with different keys, both will persist — generating the key correctly (e.g. derived from your own ID) is your responsibility. * **Doesn't affect** the server's own `4xx` responses: if the first request responded `422 invalid_request_error`, that 422 is cached. Replaying the key returns the same 422 with `Idempotent-Replayed: true`. --- # International customers (/guides/international-customers) Invoicing outside Spain raises two questions the domestic case never does: **how do you identify a recipient who has no Spanish tax ID**, and **what does the AEAT receive for an operation that is exempt, reverse-charged or located abroad**. They are independent, and this page answers them in that order. ## When this applies [#when] Whenever the recipient is not a Spanish taxpayer, or the operation is located outside mainland Spanish VAT territory. Identification is a property of the **customer**; qualification is a property of the **operation**, and the same customer can appear in operations of different kinds. ## Identifying the customer [#identity] A non-Spanish customer is identified with `alternative_id`, an object of `{type, value, country_code}` that is **mutually exclusive with the Spanish `tax_id`** ([`BR-CLI-017`](#traceability)). The type belongs to the AEAT identification catalogue, list L7, and each case has its own numeric code that travels in the VeriFactu chain: | `type` | AEAT code | Meaning | | ----------------------- | --------- | ------------------------------------------------------------- | | `nif_iva` | 02 | Intra-community VAT operator number. | | `passport` | 03 | Passport. | | `country_id` | 04 | Official identification document of the country of residence. | | `residence_certificate` | 05 | Tax residence certificate. | | `other_document` | 06 | Other supporting document. | | `not_registered` | 07 | Not listed on the AEAT census (*No censado*). | The **type-country matrix** is a hard invariant, not a suggestion: `nif_iva` is legal only for EU countries, because it *is* the intra-community operator number; the other types are valid for any non-Spanish country; and `country_code: "ES"` is always refused, because Spain uses `tax_id`. An illegal combination answers `422`: ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "name": "Müller GmbH", "alternative_id": { "type": "nif_iva", "value": "DE811569869", "country_code": "DE" } }' ``` <Callout type="info"> The legacy values `tax_id_foreign` and `national_id` are still accepted so that existing integrations do not break. Normalisation is country-aware: `national_id` becomes `country_id` unconditionally, while `tax_id_foreign` becomes `nif_iva` for an EU country and `other_document` otherwise — because a `tax_id_foreign` from outside the EU cannot be an intra-community number, and the matrix would refuse it. </Callout> If you do not send `alternative_id` at all — a foreign customer with only a country and a tax identifier — the VeriFactu chain falls back to identification type `02`, the most common intra-community case. Sending the field explicitly is strictly better. ### `vat_id` is free text, and it is not verified [#vat-id] The intra-community VAT number field accepts any string up to 20 characters. It is **not validated against the VIES registry**, not format-checked per country, and not cross-checked against `tax_id` ([`BR-CLI-003`](#traceability)). A wrong country prefix is accepted. A customer who should be under the intra-community regime but has no `vat_id` is neither blocked nor flagged. `vat_id` and `tax_id` are separate fields that coexist: a Spanish company can carry a national tax ID and the same number with the country prefix as its intra-community VAT number. ### Verifying a Spanish recipient before invoicing [#census] For recipients that **do** have a Spanish tax ID, [`POST /v1/clients/census-verification`](/api-reference/clients/public-api.v1.clients.verify_census) (scope `clients:read`) checks the name-and-tax-ID pair against the AEAT census before you invoice, anticipating the most frequent VeriFactu rejection — the one for a recipient the census does not identify ([`BR-CLI-015`](#traceability)). It is deliberately informative: it never blocks saving a customer or issuing an invoice, it persists nothing, and it is **fail-open** — an unreachable AEAT answers `200` with an unavailable status, never a `5xx`. It is throttled, because it may reach the AEAT network. See [Census verification](/guides/census-verification) for the full flow. ## The scenario map [#map] This is the map from business scenario to what the AEAT receives ([`BR-VFC-029`](#traceability)): | Scenario | Header operation regime | What reaches the AEAT | | ------------------------------------------ | ------------------------- | --------------------------------------------------------------------------------- | | Intra-community supply of **goods** | `intracomunitaria` | `E5` — subject and exempt, art. 25 LIVA | | **Services** with reverse charge | `isp` | `S2` — subject and **not** exempt, charged quota `0` (the recipient self-charges) | | **Export** outside the EU | `importacion_exportacion` | `E2` — subject and exempt, art. 21 LIVA | | Distance sales under the **one-stop shop** | (general) | `regime_key: 17` — Chapter XI of Title IX, OSS and IOSS | <Callout type="warn"> **Reverse charge is not an exemption.** It is a *qualification* derived from the header regime — `S2`, subject and not exempt, with the charged quota forced to zero because the recipient accounts for the tax. It is **not** a line exemption cause, and in particular it is **not** `E4`: that code is the exemption under arts. 23 and 24 LIVA, for customs warehouses and suspension arrangements, which is a different thing entirely. An invoice that declares reverse charge as an exempt operation misstates both the qualification and the quota. </Callout> The four qualifications reachable from the header regime are `S1` (general), `S2` (reverse charge), `E5` (intra-community) and `E2` (import or export). The other exemption codes — `E1`, `E3`, `E4`, `E6` — exist in the AEAT catalogue but are reachable only as a **line** exemption cause. ## What the API sends [#api] Here is the part that decides how you build the payload, and it is a real constraint rather than a style preference. **The header operation regime is read-only over v1.** Neither [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) nor [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update) accepts `operation_regime`; the invoice object returns it, and every invoice created through the public API is born under the general regime. The document-level exemption cause is read-only for the same reason. The customer's `preferred_operation_regime` — accepted on [`POST /v1/clients`](/api-reference/clients/public-api.v1.clients.create) with the values `general`, `intracomunitaria`, `importacion_exportacion` and `isp` — is stored and returned, but it does **not** set the regime of the invoices you create. It is a declarative preference for your own use. What you *can* express per line is the exemption cause. So: | Scenario | How you express it in v1 | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Intra-community supply of goods | `tax_rate: 0` + `exemption_reason: "E5"` per line. | | Export outside the EU | `tax_rate: 0` + `exemption_reason: "E2"`, usually with `regime_key: "02"`. | | One-stop-shop distance sales | `regime_key: "17"` per line, with the destination-country rate. | | **Reverse charge** | **Not expressible.** `S2` derives from the header regime, and the line catalogue contains no `S` codes by design. | That last row is the honest answer, and it matters: an invoice with reverse charge created through the public API will be qualified `S1` with a charged quota, which is not what you mean. Until the header regime becomes writable, issue those invoices from the dashboard. It is recorded in [Scope and limitations](/guides/scope-and-limitations). The published `intracomunitario_bienes` example on the create operation is exactly this shape — zero rate plus `E5` plus an explicit regime key — rather than a header regime it could not set: ```json { "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "notes": "Entrega intracomunitaria de bienes exenta (art. 25 LIVA)", "lines": [ { "description": "Suministro de maquinaria a cliente UE (DE)", "quantity": 1, "unit_price": 5000, "tax_rate": 0, "exemption_reason": "E5", "regime_key": "01" } ] } ``` A simplified invoice is never an option for any of these scenarios: the eligibility check blocks intra-community operations, reverse charge and any recipient outside Spain before the amount is even considered. See [Simplified or full invoices](/guides/simplified-vs-full-invoices#when). ## What appears on the PDF [#pdf] The recipient block prints the alternative identification exactly as supplied, frozen at issue time like the rest of the recipient snapshot ([`BR-INV-024`](#traceability)). The legal mention — art. 25 LIVA for an intra-community supply, art. 21 for a third-country operation, art. 84.Uno.2 for reverse charge — derives from the **header** regime, and therefore does not appear automatically on an invoice created through v1 ([`BR-TAX-024`](#traceability)). Two options: put the wording in `notes`, or use the line-level `exemption_reason_text`, which prints under the line description and is presentation only. ## What reaches the AEAT [#aeat] **In the VeriFactu record**, the recipient's identification type travels as the AEAT code from the L7 table above, and the breakdown carries the qualification described in [The scenario map](#map) — exempt-operation codes for `E5` and `E2`, `S2` with a zero quota for reverse charge. **In the annual third-party operations return** (**Modelo 347**), intra-community operations and imports or exports are **excluded** ([`BR-TXR-022`](#traceability)): they are declared through their own returns — the recapitulative statement for intra-community operations, and customs documentation for the rest — and declaring them twice would produce a cross-declaration mismatch. Reverse charge behaves the opposite way: it is a **domestic** operation and does appear in that return. The classification uses the invoice's **header** regime, so a mixed invoice is classified as a whole. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-CLI-003` — `vat_id` as free text, without VIES validation, independent of `tax_id`. * `BR-CLI-015` — census verification of the recipient: informative, fail-open and stateless. * `BR-CLI-017` — the AEAT L7 alternative-identification catalogue, the type-country matrix and the accepted legacy aliases. * `BR-INV-024` — the immutable recipient snapshot. * `BR-INV-031` — the closed regime-key catalogue used for one-stop-shop and export lines. * `BR-INV-032` — line exemption causes and their fallback to the header. * `BR-TAX-024` — the document-level exemption cause and its automatic legal mention. * `BR-VFC-029` — the qualification map: `S1`, `S2`, `E5` and `E2` derived from the header regime, and reverse charge as a qualification rather than an exemption. * `BR-TXR-022` — exclusion of intra-community and import or export operations from the annual third-party return, and the inclusion of domestic reverse charge. --- # Line tax classification and exemptions (/guides/line-tax-classification-and-exemptions) An invoice line carries more fiscal information than a rate. Four optional fields decide how the operation is classified, whether VAT is charged at all, and what the recipient actually pays: | Field | What it does | | ------------------ | ----------------------------------------------------------------------- | | `exemption_reason` | Declares the line exempt (`E1`–`E6`) or not subject (`N1`, `N2`). | | `regime_key` | Declares the special regime — see [Regime keys](/guides/regime-keys). | | `retention_rate` | IRPF withholding, **subtracted** from the amount payable. | | `surcharge_rate` | Equivalence surcharge, added — and only in legally paired combinations. | All four are optional and additive. An invoice that omits every one of them behaves exactly as it did before they existed, fingerprint included. ## When this applies [#when] Declare an exemption cause when the operation is exempt or not subject under the Spanish VAT act. Declare withholding when you invoice as a professional or lease business premises. Declare a surcharge when your customer is a retailer under the equivalence-surcharge regime. The distinction between the two families of codes is legal, not cosmetic ([`BR-INV-032`](#traceability)): | Family | Codes | LIVA basis | AEAT breakdown | | --------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------ | | **Exempt** | `E1` art. 20 · `E2` art. 21 · `E3` art. 22 · `E4` arts. 23 and 24 · `E5` art. 25 · `E6` other | The operation *is* subject to VAT, and exempted. | Declares an exempt-operation code. No VAT quota. | | **Not subject** | `N1` arts. 7, 14 and others · `N2` place-of-supply rules | The operation is outside the scope of the tax. | Declares a non-subject qualification. | <Callout type="warn"> The catalogue deliberately contains **no `S` codes**. Subject-and-not-exempt is the default, not a selectable cause, and **reverse charge is modelled at the invoice header**, not per line. Since the header regime is read-only over v1, reverse charge cannot be declared through the public API — see [International customers](/guides/international-customers#map). </Callout> ## What the API sends [#api] ### Exemption and non-subjection [#exemption] `lines[].exemption_reason` on [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) and [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). A value outside the eight-code catalogue answers `422` with `allowed_values`. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "series_id": "019e5584-7a72-7038-a8f6-561ed180b699", "issued_on": "2026-06-01", "due_on": "2026-07-01", "lines": [ { "description": "Exportación de maquinaria", "quantity": 1, "unit_price": 100, "tax_rate": 0, "exemption_reason": "E2" }, { "description": "Servicio de instalación", "quantity": 1, "unit_price": 50, "tax_rate": 21 } ] }' ``` The AEAT breakdown groups by the pair **(tax rate, exemption reason)**, so a mixed invoice produces one group per combination and each group balances on its own. Lines that share both values are aggregated into a single group. A line that omits the field falls back to the qualification derived from the invoice header. Because a v1-created invoice always has the general header regime, that fallback is "subject and not exempt" — which is why an exempt line must say so explicitly. ### IRPF withholding subtracts [#irpf] `lines[].retention_rate` is a percentage from 0 to 100, optionally paired with `lines[].retention_rate_id`, a reference to a withholding tax in your catalogue. The canonical total formula is: ``` total = subtotal + VAT − withholding + equivalence surcharge ``` Withholding is money the customer keeps back and pays to the tax authority on the professional's behalf, so it **reduces** the amount payable ([`BR-INV-033`](#traceability)): ```json { "lines": [ { "description": "Servicios de consultoría", "quantity": 1, "unit_price": 1000, "tax_rate": 21, "retention_rate": 15 } ] } ``` That line invoices 1000, charges 210 of VAT, withholds 150, and the customer pays 1060\. If you send **both** `retention_rate` and `retention_rate_id`, they must agree. A mismatch is a `422` naming both percentages, rather than a silent decision about which one wins. <Callout type="info"> Some withholding rates are stored with a negative sign — a legacy visual convention meaning "this is withheld". The calculation takes the absolute value and the subtraction is wired into the formula itself, so the sign never changes the result ([`BR-TAX-008`](#traceability)). The public tax catalogue always publishes these rates **positive**. </Callout> ### The equivalence surcharge matrix is closed [#surcharge] `lines[].surcharge_rate` is not a free number. Every line with a surcharge above zero is validated against the legal pairing with its VAT rate ([`BR-INV-034`](#traceability)): | VAT rate | Legal surcharge | | -------- | --------------- | | 21% | 5,2% | | 10% | 1,4% | | 4% | 0,5% | | 0% | 0% | An illegal combination — 21% VAT with a 1,4% surcharge, say — answers `422` with the legal pairs in `allowed_values`. Comparison is by value rounded to two decimals, so `5.2` and `5.20` are the same pair. Operations under this regime usually also carry `regime_key: "18"`. ### What comes back on each line [#line-output] The invoice line object returns `tax_rate`, `retention_rate`, `surcharge_rate`, `discount_percent`, the computed `subtotal`, `taxes` and `total`, plus the fiscal fields: `regime_key`, `exemption_reason`, `indirect_tax_regime` and `aeat_tax_code`. The last two are a **frozen fiscal snapshot**, written when the line is built and never recalculated ([`BR-TAX-023`](#traceability)). An issued invoice does not change its indirect tax regime because the company later moves its registered address, and historical lines predating the snapshot stay empty rather than being back-filled from today's data. ### Where the defaults come from [#defaults] When you omit a rate, it is resolved by a single backend chain shared by every surface — dashboard, public API, agent tooling, importers, recurring invoices — in strict priority order ([`BR-TAX-025`](#traceability)): <Steps> <Step> **Customer defaults.** The customer stores *rates*, not references, and each rate is resolved to a concrete tax **filtered by the issuer's indirect tax regime**: a customer default of 7% at a Canary Islands company resolves to IGIC at 7%, not to a mainland VAT. </Step> <Step> **Company settings**, including the suggestion derived from the company's AEAT zone. </Step> <Step> **The global catalogue.** </Step> </Steps> The chain is best-effort and **never returns an error** for an unresolvable default: it degrades to the next tier. If the customer is flagged as subject to the equivalence surcharge and the resolved VAT rate has a legally linked surcharge, that surcharge is injected into the defaults ([`BR-TAX-022`](#traceability)). Query it directly with [`GET /v1/taxes/defaults/{docType}`](/api-reference/taxes/public-api.v1.taxes.defaults) when you want to show your users what will be applied before they commit. ## What appears on the PDF [#pdf] Two things change on the printed document. **The totals block** reflects the formula above: withholding appears as a subtraction and the equivalence surcharge as an addition, so the amount payable differs from `subtotal + VAT`. **The legal mentions.** When the invoice carries a document-level exemption cause, its legal sentence — citing the LIVA article — is added as the first legal mention of the invoice ([`BR-TAX-024`](#traceability)). That cause is a **header** field, one per invoice, and it is **read-only over the public API**: the invoice object exposes `exemption_reason` and `legal_mentions`, but no v1 operation sets them. An invoice created through v1 therefore prints no automatic exemption sentence; put the wording in `notes` if the document needs it. Line-level `exemption_reason_text` (up to 255 characters) exists for the same purpose at line level, and is presentation only — it has no fiscal effect. ## What reaches the AEAT [#aeat] Per breakdown group, one qualification. A line that declares an `E` code produces an **exempt-operation** entry carrying that literal code and no charged quota; a line that declares an `N` code produces a **non-subject** qualification. A line that declares nothing inherits the header-derived qualification ([`BR-VFC-029`](#traceability)). The grouping key is the pair (tax rate, exemption reason), which is what lets a mixed invoice pass AEAT validation: each group states its own base, its own rate and its own quota, and `base × rate = quota` holds within the group. Withholding does **not** appear in the VeriFactu breakdown — it is not VAT. It is declared in the withholding returns instead, and it reduces the invoice total. The equivalence surcharge is propagated only to subject-and-not-exempt lines; exempt lines carry neither VAT nor surcharge. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-INV-032` — the closed `E1`–`E6` / `N1`–`N2` catalogue, the fallback to the header, the grouping by (rate, cause), and the identical-fingerprint invariant. * `BR-INV-033` — per-line IRPF withholding in the v1 contract and the coherence check between rate and referenced tax. * `BR-INV-034` — the closed legal matrix of VAT-to-surcharge pairs. * `BR-TAX-008` — withholding stored with a sign but computed in absolute value. * `BR-TAX-022` — the legal link from a VAT rate to its equivalence surcharge. * `BR-TAX-023` — the immutable per-line fiscal snapshot. * `BR-TAX-024` — the document-level exemption cause and the automatic legal mention. * `BR-TAX-025` — the customer → company → global chain of fiscal defaults. * `BR-VFC-029` — how the qualification is derived when the line declares no cause. --- # Migrate from Holded (/guides/migration-from-holded) This guide documents migration from the Holded API (one of the main competitors in the Spanish invoicing SaaS space) to the Factuarea Public API v1. It covers resource mapping, naming differences, equivalent endpoints and a Python sample script that migrates a whole company. ## Resource mapping [#resource-mapping] | Holded | Factuarea | Notes | | --------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contacts` | `clients` + `suppliers` | Holded mixes them in `contacts` with a `type` field. Factuarea splits them into two distinct endpoints. | | `products` | `products` | Identical naming. | | `documents/invoice` | `invoices` | Dedicated endpoint. | | `documents/estimate` | `quotes` | Name change: Holded uses "estimate", Factuarea "quote". | | `documents/proform` | `proformas` | Renamed to "proforma" without abbreviating. | | `documents/waybill` | `delivery_notes` | Canonical Spanish/legal naming. | | `documents/purchase` | `purchase_invoices` | | | `documents/recurring` | `recurring_invoices` | | | `taxes` | `taxes` | Same concept. | | `numerations` | `series` | Holded "numeration", Factuarea "series". The Holded `format` maps to `number_format`, a configurable numbering mask (padding + year token + separator), e.g. `{code}-{YYYY}-{000}`. | | `tags` | `tags` | Free classification tags on a document (lowercase slugs, ≤ 40 chars, ≤ 30 per document). | | custom fields | `custom_fields` | Typed `[{field, value}]` integration metadata on a document (≤ 50 entries). | | `webhooks` | `webhook_endpoints` (+ nested `deliveries`) | Factuarea separates endpoint configuration from delivery traceability (`GET /v1/webhook_endpoints/{id}/deliveries`). | ## Key differences [#key-differences] ### 1. Authentication [#1-authentication] * Holded: `key: <api_key>` header. * Factuarea: `Authorization: Bearer fact_live_...` or `X-API-Key: fact_live_...`. Standard OpenAPI. ### 2. Identifiers [#2-identifiers] * Holded: opaque string-numeric IDs. * Factuarea: every resource has an `id` key whose value is a **UUID v7** (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b`) — it encodes a timestamp and is lexicographically sortable. Foreign keys use `*_id` (e.g. `client_id`). <Callout type="info"> **Store the Holded ID in `external_id` — this is the recommended migration strategy.** Every Factuarea resource accepts an `external_id` (an external integration key, ≤ 100 chars, unique per company, distinct from the fiscal `tax_id`). Write the original Holded ID into it on every create. That makes the migration **natively idempotent**: you don't need to keep a `holded_id ↔ factuarea_id` mapping table — to find the Factuarea record for a Holded ID, call `POST /v1/{resource}/find-by-external-id` with body `{ "external_id": "<holded_id>" }`. Available on `clients`, `suppliers`, `products`, `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices` and `recurring_invoices`. See [external\_id in the glossary](/guides/glossary). </Callout> ### 3. Pagination [#3-pagination] * Holded: `?starttmp=...&endtmp=...` (timestamps in the URL). * Factuarea: cursor pagination (`starting_after`, `ending_before`) by resource `id`. See [Pagination](/guides/pagination). ### 4. Errors [#4-errors] * Holded: status code + `errors` array or `error` string. * Factuarea: `{ error: { type, code, message, request_id, doc_url } }` envelope. See [Errors](/guides/errors). ### 5. Webhooks [#5-webhooks] * Holded: unsigned payload (IP-based validation). * Factuarea: HMAC SHA256 signature required, ±5min tolerance, exponential retries up to 8 attempts. See [Webhooks](/guides/webhooks). ### 6. Idempotency [#6-idempotency] * Holded: not supported. * Factuarea: `Idempotency-Key` header with 24h TTL. See [Idempotency](/guides/idempotency). ## Equivalent endpoints (most common operations) [#equivalent-endpoints-most-common-operations] | Operation | Holded | Factuarea | | ------------------------ | ---------------------------------------------------- | ---------------------------------- | | List invoices | `GET /invoicing/v1/documents/invoice` | `GET /v1/invoices` | | Create invoice | `POST /invoicing/v1/documents/invoice` | `POST /v1/invoices` | | Mark invoice paid | `POST /invoicing/v1/documents/invoice/{id}/pay` | `POST /v1/invoices/{id}/mark-paid` | | Send invoice by email | `POST /invoicing/v1/documents/invoice/{id}/send` | `POST /v1/invoices/{id}/send` | | Download PDF | `GET /invoicing/v1/documents/invoice/{id}/pdf` | `GET /v1/invoices/{id}/pdf` | | List clients | `GET /invoicing/v1/contacts?type=client` | `GET /v1/clients` | | Create client | `POST /invoicing/v1/contacts` (with `type=client`) | `POST /v1/clients` | | Convert quote to invoice | `POST /invoicing/v1/documents/estimate/{id}/convert` | `POST /v1/quotes/{id}/convert` | | Create webhook | `POST /invoicing/v1/webhooks` | `POST /v1/webhook_endpoints` | ## Payload differences [#payload-differences] ### Create invoice [#create-invoice] Holded: ```json POST /invoicing/v1/documents/invoice { "contactId": "5e1c2a3b4f5d6e7f8a9b0c1d", "date": 1747314060, "items": [ { "name": "Service", "units": 1, "subtotal": 99.00, "tax": 21 } ] } ``` Factuarea: ```json POST /v1/invoices Idempotency-Key: 01928f10-7c0e-7c4a-9b7d-2f8a6e3c1d4b { "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "lines": [ { "description": "Service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] } ``` Changes: * `contactId` → `client_id` (explicit FK; value is a UUID v7). * `date` (timestamp) → `issued_on` (`YYYY-MM-DD`), with `due_on` required. * `items[].subtotal` (amount) → `lines[].unit_price` (unit price; the API calculates totals). * `items[].tax` (inline percentage) → `lines[].tax_rate_id` (FK to the tax catalog). * `series_id` required — Factuarea enforces configuring series before issuing (consistency with the Spanish tax agency). ### Webhooks: signature [#webhooks-signature] Holded doesn't sign. Factuarea does (HMAC SHA256). After migrating you **must** validate the signature in your handler. See [Webhooks](/guides/webhooks). ## Minimal migration script (Python) [#minimal-migration-script-python] <Callout type="warn"> This script is illustrative, not production-ready. Test it in staging and validate the migrated data manually before running it against production. </Callout> ```python """ Migrate contacts and products from Holded to Factuarea. Requires: pip install requests tenacity python-dotenv """ import os, time, uuid, requests from tenacity import retry, stop_after_attempt, wait_exponential HOLDED_API = 'https://api.holded.com/api/invoicing/v1' FACTUAREA_API = 'https://api.factuarea.com/v1' HOLDED_HEADERS = {'key': os.environ['HOLDED_KEY']} FACTUAREA_HEADERS = { 'Authorization': f"Bearer {os.environ['FACTUAREA_KEY']}", 'Content-Type': 'application/json', } @retry(stop=stop_after_attempt(5), wait=wait_exponential(max=10)) def fact_post(path, payload, key=None): headers = dict(FACTUAREA_HEADERS) headers['Idempotency-Key'] = key or str(uuid.uuid4()) r = requests.post(f"{FACTUAREA_API}{path}", json=payload, headers=headers, timeout=30) if r.status_code >= 500: r.raise_for_status() return r def fetch_holded_contacts(): url = f"{HOLDED_API}/contacts" while url: r = requests.get(url, headers=HOLDED_HEADERS, timeout=30) r.raise_for_status() payload = r.json() for c in payload.get('contacts', payload if isinstance(payload, list) else []): yield c url = payload.get('next') if isinstance(payload, dict) else None def migrate_clients(): migrated = 0 for h in fetch_holded_contacts(): if h.get('type') != 'client': continue payload = { 'name': h['name'], 'tax_id': h.get('code') or h.get('vatnumber'), 'email': h.get('email'), 'phone': h.get('phone'), 'address': h.get('billAddress', {}).get('address'), 'postal_code': h.get('billAddress', {}).get('postalCode'), 'city': h.get('billAddress', {}).get('city'), 'province': h.get('billAddress', {}).get('province'), 'country': h.get('billAddress', {}).get('country', 'ES'), 'external_id': h['id'], # Holded ID → external_id: the mapping key. Reconcile later via POST /v1/clients/find-by-external-id } idem_key = f"migrate-client-{h['id']}" # deterministic for retry-safety r = fact_post('/clients', {k: v for k, v in payload.items() if v is not None}, key=idem_key) if r.status_code == 201: migrated += 1 elif r.status_code == 409: # already exists (another migration run) pass else: print(f" ERROR {r.status_code} for {h['id']}: {r.text[:200]}") time.sleep(0.1) # courtesy with rate limits print(f"Clients migrated: {migrated}") if __name__ == '__main__': migrate_clients() ``` ## Bulk client import from the Holded export [#client-import] The script above creates clients one at a time through `POST /v1/clients`. If you'd rather feed Holded's **contact export file** straight in, use `POST /v1/clients/import` (scope `clients:write`, `multipart/form-data`) with the mapping preset below. It takes a `file` (CSV, XLSX, XLS, ODS or TXT, up to 10 MB), a `mapping` object and a `dry_run` flag. ### The mapping preset [#client-import-mapping] In `mapping`, the **key is the column header exactly as it appears in your file** and the **value is the destination field**. Holded localises the export headers to the account language, so open the first line of your file and adjust the keys — the values on the right never change: ```json { "Name": "name", "Trade name": "commercial_name", "VAT number": "tax_id", "EU VAT number": "vat_id", "Email": "email", "Phone": "phone", "Mobile": "mobile", "Fax": "fax", "Website": "website", "Address": "address", "City": "city", "Postal code": "postal_code", "Province": "province", "Country": "country", "IBAN": "bank_iban", "Contact person": "contact_person", "Notes": "notes" } ``` Three rules the API enforces on the mapping itself: * **`name` and `tax_id` are mandatory destinations.** A mapping without both is rejected with `422` before a single row is read. * **No destination twice.** Two headers pointing at the same field is an error, not a silent last-one-wins. * **Unmapped columns are ignored.** Holded's `Id` column is one of them — see [step 3](#client-import-reconcile). ### Destination fields [#client-import-fields] These are the fields the client importer accepts. A destination outside this table is **ignored in silence**: it is neither written nor reported as an error, exactly as if the column had not been mapped at all. Check your `mapping` against this table before the real run — a typo like `"e-mail"` costs you the whole column on every row, and the import still answers `200`. | Destination | Required | Validated as | | ------------------------ | -------- | ----------------------------------------------------- | | `name` | **yes** | non-empty | | `tax_id` | **yes** | Spanish NIF/CIF/NIE | | `commercial_name` | no | free text | | `vat_id` | no | free text | | `email` | no | email address | | `phone` | no | phone number | | `mobile` | no | phone number | | `fax` | no | free text | | `website` | no | free text | | `address` | no | free text | | `address_line2` | no | free text | | `address_number` | no | free text | | `address_floor` | no | free text | | `address_door` | no | free text | | `address_staircase` | no | free text | | `city` | no | free text | | `postal_code` | no | free text | | `province` | no | free text | | `country` | no | free text | | `bank_iban` | no | free text — becomes the client's default bank account | | `default_vat_rate` | no | numeric | | `default_retention_rate` | no | numeric | | `default_discount` | no | numeric | | `payment_method` | no | free text | | `payment_terms_days` | no | numeric | | `contact_person` | no | free text | | `notes` | no | free text | Decimals accept both `.` and `,` as separator. `tax_id` is stored upper-cased, so search for it in upper case later. Fields the importer does **not** cover — `external_id`, `billing_emails`, `alternative_id`, `metadata`, the DIR3 codes and the equivalent-surcharge flag — can only be set through `POST /v1/clients`, `POST /v1/clients/bulk-create` or `PUT /v1/clients/{id}`. ### Step 1 — dry run [#client-import-dry-run] Always start with `dry_run: true`. Nothing is written, no monthly row quota is consumed, and you get the per-row verdict: ```bash curl -X POST https://api.factuarea.com/v1/clients/import \ -H "Authorization: Bearer fact_live_..." \ -F "file=@holded-contacts.csv" \ -F 'mapping={"Name":"name","VAT number":"tax_id","Email":"email"}' \ -F "dry_run=true" ``` ```json { "data": { "object": "client_import_preview", "total_rows": 128, "rows": [ { "row": 2, "status": "valid", "errors": [], "warnings": [] }, { "row": 3, "status": "error", "errors": [ { "param": "tax_id", "code": "INVALID_FORMAT", "message": "..." } ], "warnings": [] } ] } } ``` `row` is the line number in your file — the header is line 1, so the first data row is `2`. Fix every `status: "error"` row in the source file and re-run the dry run until they're all `valid`. <Callout type="warn"> **The dry run previews the first 50 rows only.** `total_rows` counts the whole file, but `rows[]` stops at 50 — a clean dry run on a 400-row file does not mean rows 51 onwards are clean. For a large export, split it and dry-run each chunk. </Callout> ### Step 2 — the real import [#client-import-run] Same call with `dry_run=false` (or the flag omitted). Two limits apply: * **Under 200 rows per request.** A file with 200 rows or more is rejected with `422 client_import_too_large` — the import runs synchronously so it can return the per-row result in the same response. Split the export into chunks. * **A monthly row quota per plan**: 100 rows on Emprendedor, 1,000 on Empresario, unlimited on Enterprise. It counts rows actually imported, across every import of the calendar month. The import is **best-effort per row**: each row is its own transaction, so a failing row does not roll back the rows already created. The response tells you exactly which ones to resend: ```json { "data": { "total": 128, "successful": 126, "failed": 2, "failures": [ { "index": 41, "error_code": "INVALID_FORMAT", "error_message": "...", "errors": [{ "param": "tax_id", "code": "INVALID_FORMAT", "message": "..." }] } ], "results": [ { "row": 43, "status": "error", "errors": [...], "warnings": [] } ] } } ``` `index` is 0-based over the data rows; `row` is the line in the file (`index + 2`). Note that `successful` counts **created plus skipped** rows: the importer deduplicates by `tax_id`, both against clients that already exist in your company and against repeated rows inside the same file, and a skipped row is a success, not a failure. That's what makes re-running an import safe — but it also means `successful` is not the number of clients created. Only the failed rows are itemised in `results[]`. ### Step 3 — reconcile with the Holded ID [#client-import-reconcile] <Callout type="warn"> **`external_id` is not a destination field of the import.** The CSV importer writes the fields listed above and nothing else, so the Holded `Id` column cannot travel through it. Mapping `"Id": "external_id"` is **not** rejected — it is ignored in silence, and the import answers `200` as if it had worked. Stamp the id in a second pass, as below. </Callout> The pairing you need — Holded ID ↔ Spanish tax ID — is already in the export file you just uploaded. Stamp `external_id` afterwards, one call per client: 1. Resolve the client by the tax ID from that row: ```bash curl -X POST https://api.factuarea.com/v1/clients/find-by-tax-id \ -H "Authorization: Bearer fact_live_..." \ -H "Content-Type: application/json" \ -d '{ "tax_id": "B12345678" }' ``` It returns `200` with the client, or `404 client_not_found` if that row was one of the failures from step 2. 2. Write the Holded ID into it. `PUT` on a client is a partial update, so sending only `external_id` leaves every other field untouched: ```bash curl -X PUT https://api.factuarea.com/v1/clients/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01 \ -H "Authorization: Bearer fact_live_..." \ -H "Content-Type: application/json" \ -d '{ "external_id": "5e1c2a3b4f5d6e7f8a9b0c1d" }' ``` From then on the imported clients behave like the ones the script creates: `POST /v1/clients/find-by-external-id` resolves them by their Holded ID, and the invoice migration can reference them without a mapping table. <Callout type="info"> **Want `external_id` in a single pass?** Then don't use the file importer. `POST /v1/clients/bulk-create` takes up to 500 client payloads per batch, has the same `dry_run` flag and the same per-row result contract, and accepts `external_id` on every payload — read the export yourself and post it as JSON. See [Bulk operations](/guides/bulk-operations). </Callout> ## Migration checklist [#migration-checklist] 1. **Inventory**: number of contacts, products, historical invoices, active webhooks. 2. **Map via `external_id`** (recommended): write each Holded ID into the `external_id` of the corresponding Factuarea resource on create. You then don't need an intermediate `holded_id ↔ factuarea_id` table — to resolve a relationship (invoice → client) or to re-run the migration safely, look the record up with `POST /v1/{resource}/find-by-external-id` (body `{ "external_id": "<holded_id>" }`). This is what makes the migration idempotent. 3. **Phased migration**: * Catalogs: taxes, series, products → first. * Masters: clients, suppliers → second. * Historical documents: invoices, quotes, etc. → third. 4. **Temporary dual-write**: for 1–2 weeks, write to both platforms. Reconcile differences daily. 5. **Webhooks**: configure the new endpoints, deploy the handler with HMAC verification and run in parallel. 6. **Cut-over**: stop writing to Holded, disable webhooks there. 7. **Support**: contact `support@factuarea.com` with the `request_id` for any issue during the migration. ## Intentional differences [#intentional-differences] Some Holded behaviors **we don't replicate** on purpose: * **Void vs delete an invoice**: Holded lets you delete invoices. Factuarea doesn't — issuing then deleting is an anti-pattern against the Spanish tax agency. Use `POST /v1/invoices/{id}/annul` (void) or issue a corrective invoice. * **Edit an issued invoice**: Holded lets you reissue a different PDF. Factuarea blocks changes after `sent` except `mark-paid`, `annul`, `create-corrective`. This is deliberate. * **Inline VAT calculator**: Holded accepts the VAT percentage on each line. Factuarea requires an FK to the tax catalog to guarantee consistency and reporting. These are product decisions, not technical limitations. If you find a real use case we can't cover, contact product. --- # Monthly time-record close (/guides/monthly-time-close) A **monthly close** freezes the time-record register for a finished `(year, month)`. It takes a **snapshot** of every active employee's balance totals and absence breakdown — reusing the balance contract, never recomputing — and **locks the period** against retroactive entries and corrections. It is the step that turns a running ledger into a defensible monthly record. All endpoints live under `https://api.factuarea.com/v1`; closing and reopening use `time_entries:write`, reads and exports use `time_entries:read` (payroll exports use `payroll_exports:read`). ## Close a month [#close] `POST /v1/monthly-register-closes` closes a finished month. `year` and `month` (1–12) are required. A month **that has not ended yet** returns `422`; a period **already closed** returns `409`. The close is created in status `closed` and the response carries a `Location` header to it. ```bash curl -X POST https://api.factuarea.com/v1/monthly-register-closes \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "year": 2026, "month": 1 }' ``` A close moves between two states, `closed ⇄ reopened`; neither is terminal. **Reopening** (`POST /v1/monthly-register-closes/{close}/reopen`) is an audited recovery of an erroneous close that re-enables writes for the period. Re-closing a reopened month keeps its original `id` — the reopened-and-re-closed close is the **same** resource. List closes with `GET /v1/monthly-register-closes` (ordered by period descending, filterable by `year`) and fetch one with `GET /v1/monthly-register-closes/{close}`. ## Seal it with a digital signature [#seal] `POST /v1/monthly-register-closes/{close}/seal` **seals** a `closed` register: it freezes a canonical SHA-256 digest of the snapshot and a detached **RSA-SHA256 signature** made with the company certificate. The register becomes tamper-evident and independently verifiable by a third party. There is **one seal per close** — re-sealing returns `409`. Sealing a close that is not `closed` returns `422`, and a company without an active, usable certificate returns `422`. Retrieve the seal and its **live verification state** with `GET /v1/monthly-register-closes/{close}/seal`: `verified` is `true` when the snapshot and signature are intact, otherwise `verification_reason` explains the mismatch (`snapshot_mismatch`, `signature_invalid` or `certificate_unreadable`). ```bash curl -X POST https://api.factuarea.com/v1/monthly-register-closes/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/seal \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` <Callout type="info"> The seal is optional but recommended: the close alone locks the period, and the seal adds a cryptographic signature that lets an auditor prove the snapshot has not changed since it was signed. </Callout> ## Report and exports [#exports] Three read outputs are built from the **frozen snapshot**, so the totals never drift from the sheet at the moment of closing. | Output | Endpoint | What you get | | ------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monthly report | `GET /v1/monthly-register-closes/{close}/report` | Company aggregate totals plus one row per employee (totals, absence breakdown, balances) and the daily detail. Totals in minutes. | | Daily record export | `GET /v1/monthly-register-closes/{close}/export` | The daily record as a spreadsheet in the `rdley_8_2019` format, read from the locked ledger. Binary download. | | Payroll incidents | `GET /v1/monthly-register-closes/{close}/payroll-export` | One row per employee (fiscal identity, worked vs expected minutes, overtime, balance, approved absences by type) in `a3`, `sage` or `nominasol`. Binary download. | The report is a **computed resource**: it exposes `close_id`, never an `id` of its own. For the export and payroll files, `format` is optional (defaulting to `rdley_8_2019` and `a3` respectively); a value outside the catalogue returns `422`, and a period without a close returns `404`. List the supported payroll software with `GET /v1/payroll-export-formats`. ```bash curl -G https://api.factuarea.com/v1/monthly-register-closes/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/payroll-export \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "format=a3" \ --output payroll-2026-01.xlsx ``` See the schemas in the [API Reference](/api-reference/monthly-register-closes/public-api.v1.monthly_time_record_closes.create). ## Typical flow [#flow] 1. **Close** the finished month (`POST .../monthly-register-closes`). 2. **Seal** it if you need a signed, verifiable record (`POST .../{close}/seal`). 3. **Report or export** for auditing (`/report`), the inspection file (`/export`) or payroll (`/payroll-export`). 4. If you spot an error, **reopen**, fix the entries and **re-close** — the `id` stays the same. ## Next steps [#next] * [Time clock](/guides/time-clock) — the entries and corrections the close snapshots. * [Absences](/guides/absences) — the approved absences that appear in the report. --- # Pagination (/guides/pagination) Every list endpoint in the public API uses **cursor pagination**. Same semantics as Stripe / Linear: you paginate by an opaque identifier (the resource `id`), not by page number. This guarantees stable results even when new resources are created during iteration. ## Parameters [#parameters] | Parameter | Type | Default | Range | Description | | ---------------- | ------- | ---------- | ------------------ | --------------------------------------------------------------------------- | | `limit` | integer | `25` | `1`–`100` | Number of items per page. | | `starting_after` | string | `null` | id (UUID v7) | Returns items created **after** the resource whose `id` is passed. | | `ending_before` | string | `null` | id (UUID v7) | Returns items created **before** the resource whose `id` is passed. | | `sort` | string | `-created` | per-resource field | Sort field; `-` prefix for descending (e.g. `-total`). See [Order](#order). | <Callout type="warn"> `starting_after` and `ending_before` are mutually exclusive. Sending both in the same request responds `422` with a `invalid_request_error` envelope. </Callout> ## Response shape [#response-shape] ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "...": "..." }, { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "...": "..." } ], "has_more": true, "next_cursor": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c" } ``` * `data`: array of up to `limit` items, ordered by `id` DESC (equivalent to `created_at` DESC because we use UUID v7). * `has_more`: `true` if there are more items before the first one in `data` (newer) when paginating with `starting_after`, or after the last when paginating with `ending_before`. * `next_cursor`: `id` of the last item in `data`. Pass it as `starting_after` in the next request to move forward. When there are no more items `has_more` is `false` and `next_cursor` is `null`. ## Iterate all results [#iterate-all-results] <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests def iterate(endpoint): cursor = None while True: params = {'limit': 100} if cursor: params['starting_after'] = cursor resp = requests.get( f'https://api.factuarea.com/v1/{endpoint}', params=params, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() body = resp.json() yield from body['data'] if not body['has_more']: break cursor = body['next_cursor'] for invoice in iterate('invoices'): print(invoice['id'], invoice['number']) ``` </Tab> <Tab value="Node.js"> ```javascript async function* iterate(endpoint) { let cursor = null; while (true) { const url = new URL(`https://api.factuarea.com/v1/${endpoint}`); url.searchParams.set('limit', '100'); if (cursor) url.searchParams.set('starting_after', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}` }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const body = await res.json(); for (const item of body.data) yield item; if (!body.has_more) break; cursor = body.next_cursor; } } for await (const invoice of iterate('invoices')) { console.log(invoice.id, invoice.number); } ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash cursor="" while : ; do if [ -z "$cursor" ]; then url="https://api.factuarea.com/v1/invoices?limit=100" else url="https://api.factuarea.com/v1/invoices?limit=100&starting_after=$cursor" fi resp=$(curl -s -H "Authorization: Bearer $FACTUAREA_API_KEY" "$url") echo "$resp" | jq -c '.data[]' has_more=$(echo "$resp" | jq -r '.has_more') cursor=$(echo "$resp" | jq -r '.next_cursor') [ "$has_more" = "true" ] || break done ``` </Tab> </Tabs> ## Iterate with the official SDK [#iterate-with-the-official-sdk] The [TypeScript and PHP SDKs](/sdks) hide the cursor entirely: list methods return an iterator that walks every page for you. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts const page = await factuarea.invoices.list({ status: "paid", limit: 50 }); // iterate every item across every page — cursors handled internally for await (const invoice of page) { console.log(invoice.id, invoice.number); } // or walk page by page page.data; // items on this page page.hasMore; // boolean page.nextCursor; // opaque cursor or null const next = await page.getNextPage(); // Page | null const all = await page.toArray(); // collect everything ``` </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\Pagination\PageIterator; use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest; $pages = new PageIterator( fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList( new PublicApiV1InvoicesListRequest(startingAfter: $cursor), )->rawResponse, ); foreach ($pages->items() as $invoice) { echo $invoice['id'], PHP_EOL; } ``` </Tab> </Tabs> See [SDKs › Paginating with the SDK](/sdks#paginating-with-the-sdk) for the full surface. ## Order [#order] Pass `?sort=<field>` to order a list. A bare field sorts **ascending**; a `-` prefix sorts **descending** (e.g. `?sort=-total`). When omitted, the default is **`-created`** — equivalent to `id` DESC, a stable total order because we use UUID v7 (which encodes a timestamp in the high 48 bits, so "most recently created first" needs no extra `created_at` column). The cursor (`starting_after` / `ending_before`) keeps working with the order you choose: the chosen field is the primary sort and the resource `id` is a stable secondary sort, so pagination stays deterministic even when rows share the same value (Stripe-style). Allowed `sort` fields are scoped **per resource** — sending an unsupported field responds `422` with a `invalid_request_error` envelope: | Resource | Allowed `sort` fields | | -------------------- | ------------------------------------------- | | `invoices` | `created`, `total`, `number` | | `quotes` | `created`, `total`, `number`, `valid_until` | | `proformas` | `created`, `total`, `number`, `valid_until` | | `delivery_notes` | `created`, `number`, `delivery_date` | | `purchase_invoices` | `created`, `total`, `issued_on`, `due_on` | | `recurring_invoices` | `created`, `next_run_at` | ```bash # Invoices, highest total first curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "sort=-total" # Quotes by expiry date, soonest first curl -G https://api.factuarea.com/v1/quotes \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "sort=valid_until" ``` ## Why not `?page=`? [#why-not-page] Numeric pages have problems when the dataset changes during iteration: * Creating a resource between pages → duplicates rows. * Deleting a resource between pages → drops rows. * `COUNT(*)` is expensive past a few thousand rows. Cursor pagination with UUID v7 eliminates both: the cursor points to a stable position in time, not a variable offset. That's why there is no `?page=N` offset parameter — the only way to page through a list is the `starting_after` / `ending_before` cursor. An invalid cursor value responds `422` with a `invalid_request_error` envelope (`code: parameter_invalid_cursor`): ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid_cursor", "message": "The provided cursor is not a valid resource id.", "param": "starting_after", "request_id": "req_..." } } ``` ## Use case: fetch only new results [#use-case-fetch-only-new-results] If your integration polls every N minutes, save the `next_cursor` (the newest `id` you've seen) between pollings. On the next pass use `ending_before=<saved_cursor>` to fetch only items **newer** than that point. ```python last_seen = load_last_cursor() # resource id stored in your DB resp = requests.get( 'https://api.factuarea.com/v1/invoices', params={'limit': 100, 'ending_before': last_seen} if last_seen else {'limit': 100}, headers={'Authorization': f"Bearer {API_KEY}"}, ) new_invoices = resp.json()['data'] if new_invoices: save_last_cursor(new_invoices[0]['id']) # the newest one ``` --- # Recording payments (/guides/payments) Invoices and purchase invoices keep a **payment ledger**: a list of individual payments, each with its own amount, date and method. Register payments one at a time as the money comes in — the API recomputes the **paid** and **pending** amounts after every entry and flips the document to `paid` once the balance reaches zero. There is no separate "partially paid" status. The progress of collection is read from two derived, display-only fields on the invoice: `paid_amount` (sum of the ledger) and `pending_amount` (`total − paid_amount`). A document with `pending_amount > 0` is still `pending`; the one whose `pending_amount` hits `0` becomes `paid`. ## Register a sale payment [#register-a-sale-payment] `POST /v1/invoices/{id}/payments` adds one payment to a sales invoice. The body is small: | Field | Type | Required | Notes | | ---------------- | --------------------- | -------- | ------------------------------------------------- | | `amount` | number | **Yes** | Greater than `0`. Cannot exceed `pending_amount`. | | `paid_on` | string (`YYYY-MM-DD`) | **Yes** | The date the money was received. | | `payment_method` | string (enum) | **Yes** | One of the catalog values (see below). | | `reference` | string | No | Your own reference (e.g. a transfer number). | | `notes` | string | No | Free internal note. | `payment_method` is a closed enum of seven values: `bank_transfer`, `direct_debit`, `cash`, `credit_card`, `check`, `paypal`, `other`. Fetch the labelled catalog from [`GET /v1/payment-methods`](#payment-methods) instead of hardcoding them. The response is `201 Created` with the freshly created payment under `data`: ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } } ``` <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests resp = requests.post( 'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments', json={ 'amount': 500.00, 'paid_on': '2026-05-20', 'payment_method': 'bank_transfer', 'reference': 'TRF-2026-0042', }, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() payment = resp.json()['data'] print(payment['id'], payment['amount']) ``` </Tab> <Tab value="Node.js"> ```javascript const res = await fetch( 'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 500.0, paid_on: '2026-05-20', payment_method: 'bank_transfer', reference: 'TRF-2026-0042', }), }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(data.id, data.amount); ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash curl -s -X POST \ https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 500.00, "paid_on": "2026-05-20", "payment_method": "bank_transfer", "reference": "TRF-2026-0042" }' | jq '.data' ``` </Tab> </Tabs> ## Partial payments & balance [#partial-payments--balance] The running balance does **not** live on the payment object — it lives on the **invoice**. After registering one or more payments, read the invoice (`GET /v1/invoices/{id}`) to see where it stands: ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "object": "invoice", "status": "pending", "total": 1210.00, "paid_amount": 500.00, "pending_amount": 710.00, "payments": { "detail": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } ], "total": 500.00, "pending": 710.00 } } } ``` * `paid_amount` / `pending_amount` — the collected and outstanding totals. Always present, computed from the ledger. * `payments.total` / `payments.pending` — the same two figures, mirrored inside the `payments` object. Always present. * `payments.detail` — the array of individual payments. Materialised only on the **show** endpoint (`GET /v1/invoices/{id}`); in **list** endpoints it comes back as `[]` (while `total` and `pending` stay populated) to keep listings cheap. Use the [sub-resource](#list-payments) for the detail on its own. Once the last payment closes the balance (`pending_amount` reaches `0`), the invoice transitions to `paid`. <Callout type="warn"> A payment whose `amount` is greater than `pending_amount` is rejected with `422` and `subcode: "payment_exceeds_pending_amount"` (`param: "amount"`). A payment **exactly equal** to the pending amount is valid and settles the invoice. See [Errors](/guides/errors#business_rule_violation). </Callout> ### List payments [#list-payments] `GET /v1/invoices/{id}/payments` returns the full ledger of one invoice, newest first. An invoice with no payments returns `{ "data": [] }` — never a `404`. ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "payment", "invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "amount": 500.00, "payment_date": "2026-05-20", "payment_method": "bank_transfer", "payment_method_text": "Transferencia bancaria", "reference": "TRF-2026-0042", "notes": null, "created_at": "2026-05-20T10:30:00Z", "updated_at": "2026-05-20T10:30:00Z" } ] } ``` ## Purchase invoice payments [#purchase-invoice-payments] Purchase invoices keep their own ledger (`total_retention` for IRPF withholding lives on the purchase invoice resource). The contract is **asymmetric** to the sales side — read it carefully before reusing code: * `POST /v1/purchase_invoices/{id}/payments` returns `201` with the **created payment** under `data` (object `purchase_invoice_payment`), not the full invoice. * `GET /v1/purchase_invoices/{id}/payments` returns `{ "data": [...] }`, newest first. * The body adds an optional `bank_account_id` (integer), and `payment_method` here is a **free string** (max 30 chars), not the closed enum used on the sales side. | Field | Type | Required | Notes | | ----------------- | --------------------- | -------- | --------------------------------------------------- | | `amount` | number | **Yes** | Greater than `0`. Cannot exceed the pending amount. | | `paid_on` | string (`YYYY-MM-DD`) | **Yes** | Between the issue date and today. | | `payment_method` | string | **Yes** | Free text, max 30 chars. | | `bank_account_id` | integer | No | Bank account the payment was made from. | | `reference` | string | No | Your own reference. | | `notes` | string | No | Free internal note. | ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c", "object": "purchase_invoice_payment", "amount": 423.50, "paid_on": "2026-05-21", "payment_method": "transferencia", "bank_account_id": 12, "reference": "TRF-2026-0099", "notes": null, "created_at": "2026-05-21T09:00:00Z" } } ``` <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests resp = requests.post( 'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments', json={ 'amount': 423.50, 'paid_on': '2026-05-21', 'payment_method': 'transferencia', 'bank_account_id': 12, }, headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"}, ) resp.raise_for_status() print(resp.json()['data']['id']) ``` </Tab> <Tab value="Node.js"> ```javascript const res = await fetch( 'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 423.5, paid_on: '2026-05-21', payment_method: 'transferencia', bank_account_id: 12, }), }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); console.log(data.id); ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash curl -s -X POST \ https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 423.50, "paid_on": "2026-05-21", "payment_method": "transferencia", "bank_account_id": 12 }' | jq '.data' ``` </Tab> </Tabs> <Callout type="info"> Purchase-invoice payment rules (`BR-PUR-019`) are enforced as `422`: an amount above the pending balance (`subcode: "payment_exceeds_pending_amount"`), a date outside `issue_date … today` (`subcode: "invalid_payment_date"`), or a payment on a cancelled invoice (`subcode: "purchase_invoice_not_payable"`). </Callout> ## Payment methods [#payment-methods] `GET /v1/payment-methods` returns the closed catalog backing the sales `payment_method` field, each with a `value` and a human label (Spanish). It is a global enum catalog — not tenant-specific. ```json { "data": [ { "value": "bank_transfer", "label": "Transferencia bancaria" }, { "value": "direct_debit", "label": "Domiciliación bancaria" }, { "value": "cash", "label": "Efectivo" }, { "value": "credit_card", "label": "Tarjeta de crédito" }, { "value": "check", "label": "Cheque" }, { "value": "paypal", "label": "PayPal" }, { "value": "other", "label": "Otro" } ] } ``` Read it once at startup and present the labels in your UI; send the `value` back in `payment_method`. ## Errors [#errors] * **`422` `payment_exceeds_pending_amount`** — the amount is larger than the outstanding balance (`param: "amount"`). This is a business-rule violation, so it is `422`, never `409`. * **`409`** on a payment `POST` is reserved for the standard [idempotency](/guides/idempotency) / conflict envelope (a reused `Idempotency-Key` with a different body, or a concurrency conflict) — not for the payment data itself. See [Errors](/guides/errors) for the full envelope and code catalog. --- # Presence (/guides/presence) **Presence** answers two live questions: **who is working right now?** and **who is in the office and who is remote today?** It is not a CRUD over a table of its own — it is a **derived read-model** composed from three sources: the employee **roster**, the **clock state** derived from the time-record ledger, and the **schedule** in force. The live work state (`working`, `paused`, `finished`, `away`) and the late-arrival flag are **computed on read**, never persisted. Over the v1 API, presence is **read-only** (`presence:read`), under `https://api.factuarea.com/v1`. There is no `presence:write` scope: declaring office/remote presence is a portal-only task performed by the employee. ## The live team panel [#live] `GET /v1/presence` returns the live panel: one item per active employee with their current work state, since when the current shift has been open, and whether they arrived late against their [scheduled](/guides/work-schedules) start time, plus aggregate counters (working, paused, away, remote). ```bash curl https://api.factuarea.com/v1/presence \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` The work state is derived from the last entry of each employee's open shift in the ledger: `clock_in`/`pause_end` → `working`, `pause_start` → `paused`, `clock_out` → `finished`, no open shift → `away`. Late arrival compares the first clock-in of the day against the planned start read from the employee's schedule. ## Daily office/remote presence [#daily] `GET /v1/presence/daily` lists **daily presence** — office vs remote — with filters by employee, date or range and cursor pagination. `GET /v1/presence/{employee}` returns the presence of a single employee by their `id` (UUID v7); an employee of another company returns `404`. ```bash curl -G https://api.factuarea.com/v1/presence/daily \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "date=2026-02-03" ``` **Daily presence** (office or remote) is the one datum presence actually stores: one record per employee and day. Declaring it again for the same day changes the location rather than creating a duplicate. See the schemas in the [API Reference](/api-reference/presence/public-api.v1.presence.live). <Callout type="warn"> Presence is read-only over the API. Employees declare whether they are in office or remote from the portal — there is no public write endpoint, so an integration reads presence, it does not set it. </Callout> ## Typical flow [#flow] 1. Poll `GET /v1/presence` for a live dashboard of who is working, paused or away. 2. Read `GET /v1/presence/daily` to see the office/remote split for a date. 3. Drill into one person with `GET /v1/presence/{employee}`. Because presence is derived, the numbers always reflect the current state of the ledger and schedules — you never need to keep a separate presence table in sync. ## Next steps [#next] * [Time clock](/guides/time-clock) — the ledger the live state is derived from. * [Work schedules](/guides/work-schedules) — the planned start the late-arrival flag uses. --- # Quickstart (/guides/quickstart) This guide takes you from a fresh API key to a real (sandbox) invoice in five steps. Every call below is **copy-paste runnable** against a `fact_test_` key — no real emails, no AEAT submission, no production numbering consumed. See [Test mode & sandbox](/guides/test-mode) for what "test" switches off. <Callout type="info"> **Prefer an SDK?** If you're on TypeScript/Node or PHP, the [official SDKs](/sdks) wrap this whole flow with built-in retries, idempotency, cursor pagination and typed errors — `npm install @factuarea/sdk` or `composer require factuarea/factuarea-php`. The raw HTTP steps below work in any language and show exactly what the SDK sends under the hood. </Callout> <Callout type="info"> Run everything with a **`fact_test_`** key first. The API surface is identical in live and test — when your flow works end-to-end, swap the prefix to `fact_live_` to go to production. Get a test key from [Settings → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys). </Callout> Export your key once so every snippet picks it up: ```bash export FACTUAREA_API_KEY="fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" ``` The base URL is `https://api.factuarea.com/v1`. Authenticate with `Authorization: Bearer` (or the equivalent `X-API-Key` header). Identifiers are opaque `id` values (UUID v7); you copy them from one response into the next. <Steps> <Step> **Verify your key** `GET /v1/account` introspects the credential: the company it belongs to, the plan, the API access status and the **scopes** and rate-limit **tier** of the key itself (derived from the plan). It needs the `account:read` scope. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl https://api.factuarea.com/v1/account \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` </Tab> </Tabs> ```json { "data": { "object": "account", "company": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Acme Soluciones SL", "tax_id": "B12345678" }, "plan": { "slug": "empresario", "name": "Empresario" }, "addon": { "active": true, "in_grace": false, "expires_at": null }, "api_key": { "id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "name": "Sandbox integration", "prefix": "fact_test_3pXnR2Vb", "scopes": [ "account:read", "series:read", "taxes:read", "clients:write", "invoices:write", "invoices:send", "pdfs:read" ], "tier": "pro", "created_at": "2026-05-01T09:30:00Z", "last_used_at": "2026-06-02T08:12:00Z", "expires_at": null } } } ``` A `200` here means the key is valid and you can see exactly which scopes it carries. If you get `401 invalid_api_key`, re-check the value; if a later step fails with `403 insufficient_scope`, the `scopes` array above tells you what's missing. </Step> <Step> **Grab the ids you'll need** An invoice references a **series** (its numbering) and each line references a **tax rate**. Both are existing resources you list once and reuse. **A series id** `GET /v1/series` returns your numbering series. Pick one whose `document_type` is `invoice` (the `is_default` one is a safe choice). Needs `series:read`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl "https://api.factuarea.com/v1/series?document_type=invoice" \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` </Tab> </Tabs> ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "object": "series", "code": "F-2026", "name": "Facturas 2026", "document_type": "invoice", "prefix": "F-2026-", "next_number": 46, "year_reset": true, "is_default": true, "is_active": true, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-20T11:30:00Z" } ], "has_more": false, "next_cursor": null } ``` Copy the `id` (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e`) — that's your `series_id`. **A tax rate id** `GET /v1/taxes` returns the tax catalog (global system taxes + your custom ones). For a standard Spanish invoice line you want the VAT 21% rate — look for `type: "vat"` and `rate: 21`. Needs `taxes:read`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl "https://api.factuarea.com/v1/taxes?type=vat" \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` </Tab> </Tabs> ```json { "data": [ { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", "object": "tax", "name": "IVA general 21%", "code": "IVA21", "rate": 21, "type": "vat", "applies_to": "both", "country": "ES", "is_default": true, "is_active": true, "is_system": true } ], "has_more": false, "next_cursor": null } ``` Copy this `id` — on an invoice line it goes into `tax_rate_id`. <Callout type="info"> **`tax_rate_id` vs `tax_rate`.** On a line you can reference a catalog rate by `tax_rate_id`, or skip the lookup and pass the numeric percentage directly as `tax_rate` (e.g. `"tax_rate": 21`). Use one or the other per line — `tax_rate_id` keeps the line tied to your catalog, `tax_rate` is a quick inline override. </Callout> </Step> <Step> **Create a client** The invoice needs someone to bill. The minimal client body is `name` plus `tax_id` (the Spanish fiscal identifier — NIF/CIF/NIE). Needs `clients:write`. This is a write — send an `Idempotency-Key` so a retried request never creates a duplicate client. See [Idempotency](/guides/idempotency). <Tabs items="['curl']"> <Tab value="curl"> ```bash curl -X POST https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Cliente Demo SL", "tax_id": "B98765432" }' ``` </Tab> </Tabs> ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "object": "client", "name": "Cliente Demo SL", "commercial_name": null, "tax_id": "B98765432", "vat_id": null, "email": null, "phone": null, "contact_person": null, "billing_emails": [], "address": { "line1": null, "postal_code": null, "city": null, "province": null, "country": null }, "coordinates": null, "notes": null, "metadata": {}, "is_active": true, "created_at": "2026-06-02T10:30:00Z", "updated_at": "2026-06-02T10:30:00Z" } } ``` (Optional fields you didn't send come back as `null`; `address` is always an object whose sub-keys are filled in as you provide them.) Copy the returned `id` (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01`) — that's your `client_id`. </Step> <Step> **Create the invoice** Now combine the three ids. `POST /v1/invoices` requires `client_id`, `series_id`, `issued_on`, `due_on` and at least one line. Each line needs `description`, `quantity` and `unit_price`; add `tax_rate_id` (or `tax_rate`) to apply VAT. Optional per line: `discount_percent` and `product_id`. Needs `invoices:write`. <Tabs items="['curl']"> <Tab value="curl"> ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "issued_on": "2026-06-02", "due_on": "2026-07-02", "lines": [ { "description": "Consultoría — junio 2026", "quantity": 10, "unit_price": 100, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", "discount_percent": 0 } ] }' ``` </Tab> </Tabs> The API computes the line and document totals for you and returns the invoice envelope. A freshly created invoice starts as a **draft**: no definitive `number` yet (`is_number_assigned: false`). ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42", "object": "invoice", "number": null, "is_number_assigned": false, "type": "F1", "series": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", "code": "F-2026" }, "client": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "name": "Cliente Demo SL" }, "status": "draft", "issued_on": "2026-06-02", "due_on": "2026-07-02", "subtotal": 1000, "taxes_total": 210, "total": 1210, "currency": "EUR", "notes": null, "lines": [ { "object": "invoice_line", "description": "Consultoría — junio 2026", "product": null, "quantity": 10, "unit_price": 100, "tax_rate": 21, "discount_percent": 0, "subtotal": 1000, "taxes": 210, "total": 1210 } ], "metadata": {}, "operation_regime": "general", "verifactu_status": "not_applicable", "is_corrective": false, "corrective": null, "payment": null, "public_link": null, "substituted_by": null, "recurring": null, "paid_at": null, "paid_on": null, "sent_at": null, "voided_at": null, "void_reason": null, "created_at": "2026-06-02T10:31:00Z", "updated_at": "2026-06-02T10:31:00Z" } } ``` Notice the computed money fields: the line `subtotal` (`10 × 100 = 1000`), its `taxes` (`21%` of `1000 = 210`) and `total` (`1210`), aggregated into the invoice's `subtotal` / `taxes_total` / `total`. Copy the invoice `id` (`01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42`) for the next step. </Step> <Step> **Get the PDF and send it** With the invoice `id` you can download its PDF and email it to the client. `GET /v1/invoices/{id}/pdf` streams the binary PDF (`application/pdf`). Needs `pdfs:read`. Save it straight to a file with curl's `-o`: <Tabs items="['curl']"> <Tab value="curl"> ```bash curl https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/pdf \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -o invoice.pdf ``` </Tab> </Tabs> `POST /v1/invoices/{id}/send` emails the invoice to the client. With no body it uses the client's email on file; you can override the recipient and copy with `to`, `cc`, `bcc`, `subject` and `body`. Needs `invoices:send`. <Callout type="info"> Because you're on a `fact_test_` key, the email is **not delivered** to any real recipient (sandbox effects are off). The call still succeeds and the invoice transitions as it would in production — perfect for wiring up your flow without spamming anyone. </Callout> <Tabs items="['curl']"> <Tab value="curl"> ```bash curl -X POST https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/send \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "to": "demo@example.com", "subject": "Tu factura de Acme Soluciones SL" }' ``` </Tab> </Tabs> The response is the updated invoice envelope (same shape as above), now with `sent_at` populated. </Step> </Steps> ## That's it [#thats-it] You verified a key, discovered the ids it needs, created a client, issued an invoice with server-computed totals, and sent it — all against an isolated sandbox. From here, point the same code at a `fact_live_` key to operate on your real company. <Cards> <Card icon="<Package />" title="Official SDKs" href="/sdks"> Run this same flow with @factuarea/sdk (TypeScript) or factuarea/factuarea-php — retries, idempotency, pagination and typed errors included. </Card> <Card icon="<FlaskConical />" title="Test mode & sandbox" href="/guides/test-mode"> What a fact\_test\_ key switches off, how the sandbox company is isolated, and how to promote your integration to production. </Card> </Cards> --- # Rate limits (/guides/rate-limits) The public API enforces two quota levels to guarantee fairness between tenants and protect the backend from bursts: 1. **Per-minute quota** (sliding window). 2. **Monthly quota** (natural calendar, resets on day 1 at 00:00 Europe/Madrid). The limits depend on your API key **tier**. The tier is **derived from your company's Factuarea plan** (or from an active [capacity boost](#capacity-boost) when higher) — it is never set per key or per request, and it updates automatically when your plan changes. ## Tiers [#tiers] | Tier | Per minute | Per month | Included with | | ----------- | ---------- | ------------ | ------------------------------------------ | | **Free** | 10 rpm | 100 requests | The 10-day trial. | | **Starter** | 30 rpm | 5,000 | The Emprendedor plan. | | **Pro** | 300 rpm | 50,000 | The Empresario plan. | | **Scale** | Custom | Custom | The Enterprise plan (or a capacity boost). | Tiers are cumulative: once the monthly quota is exhausted you get `429 rate_limit_exceeded` until day 1 of the next month. The per-minute quota resets on a sliding window. ## Capacity boost [#capacity-boost] If you need more API capacity without changing plans, subscribe to a **capacity boost** from the dashboard: a tier **strictly higher** than the one your plan grants (for example, Starter → Pro). While the boost is active, all your keys use the boosted tier. Buying a tier equal to or lower than your plan's returns `422 boost_not_applicable`. ## Sliding window [#sliding-window] The per-minute bucket is **not** a fixed window "60 seconds since 12:00". It's a sliding window: at any point, the API counts how many accepted requests there are in the last 60 seconds for your key. When the counter equals the limit, subsequent requests respond `429` until enough time has passed for the early requests to "drop off" the window. <Callout type="info"> **Why**: there's no "grace minute" every 60 seconds where you could send twice the limit. Fairer and more stable under real traffic. </Callout> ## Response headers [#response-headers] Every response (including 429) includes: | Header | Meaning | | ----------------------- | ---------------------------------------------------------------- | | `X-RateLimit-Limit` | Per-minute limit of your tier. | | `X-RateLimit-Remaining` | Requests remaining in the current window. | | `X-RateLimit-Reset` | UNIX timestamp when a slot frees up (one slot exits the window). | | `Retry-After` | Only on `429`. Seconds until you can retry. | Example headers on a `200` response: ```http HTTP/1.1 200 OK X-RateLimit-Limit: 300 X-RateLimit-Remaining: 287 X-RateLimit-Reset: 1747314060 ``` And on a `429`: ```http HTTP/1.1 429 Too Many Requests Retry-After: 7 X-RateLimit-Limit: 30 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1747314007 ``` ## Error code [#error-code] ```json { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Has superado el límite de peticiones. Vuelve a intentarlo en unos segundos.", "request_id": "req_..." } } ``` Exceeding the per-minute or monthly quota responds `429` with `type: rate_limit_error` and `code: rate_limit_exceeded`. The `Retry-After` header (and the message) tell you how long to wait. Repeated authentication failures are throttled separately with `code: too_many_auth_failures`. ## Best practices [#best-practices] ### 1. Respect Retry-After [#1-respect-retry-after] ```python import time, requests def call_with_retry(url, **kwargs): while True: resp = requests.get(url, **kwargs) if resp.status_code != 429: return resp sleep = int(resp.headers.get('Retry-After', 1)) time.sleep(sleep) ``` ### 2. Exponential back-off with jitter [#2-exponential-back-off-with-jitter] For `5xx`, where there's no `Retry-After`: ```python import random, time def backoff(attempt): return min(60, (2 ** attempt) * 0.1 + random.uniform(0, 0.5)) for attempt in range(5): resp = requests.get(url) if resp.status_code < 500: break time.sleep(backoff(attempt)) ``` ### 3. Monitor X-RateLimit-Remaining [#3-monitor-x-ratelimit-remaining] If your integration consistently approaches 10% of the limit, consider: * Upgrading tier. * Batching: instead of N POSTs, aggregate and do 1 POST. * Caching frequent reads (products, taxes, series). * Subscribing to webhooks instead of polling. ### 4. Webhooks > polling [#4-webhooks--polling] If you poll `/v1/invoices?status=paid` every minute to detect payments you consume 30 rpm just for that. Subscribe to the `invoice.paid` event and drop that to 0 requests. ### 5. Per-integration keys [#5-per-integration-keys] If you have two integrations (an internal dashboard + an export cron), create **two distinct keys**: each key has its own per-minute and monthly buckets, so a heavy cron doesn't exhaust the budget of an interactive dashboard. ## Administrative quotas [#administrative-quotas] Some endpoints have additional quotas **independent** of the main rate limit: | Endpoint | Quota | | ----------------------------- | --------------------------------------------------- | | `POST /v1/webhook_endpoints` | Limited number of endpoints per company. | | `POST /v1/invoices/{id}/send` | Rate-limited per invoice to avoid duplicate emails. | | `GET /v1/invoices/{id}/pdf` | Rate-limited PDF generations per minute. | These limits respond `429` with `type: rate_limit_error` and a specific message. ## Tier upgrade [#tier-upgrade] Changing tier does not require rotating keys. When your plan changes (or a capacity boost activates): 1. New quotas apply immediately. 2. The monthly quota consumed at the previous tier **does not reset**: only the monthly cap grows. 3. Existing keys keep their `id`; the new tier applies to all of them automatically. --- # Recurring invoices (/guides/recurring-invoices) A **recurring invoice** is a template plus a cadence: Factuarea generates a real invoice on every scheduled run. This guide covers the controls that go beyond basic create/update — skipping a cycle, bootstrapping a recurrence from an existing invoice, automatic email delivery, previewing the computed document and the per-line fiscal fields that VeriFactu requires. All endpoints below live under `https://api.factuarea.com/v1` and use the same [error envelope](/guides/errors), [cursor pagination](/guides/pagination) and [scopes](/guides/scopes-and-irreversibility) as the rest of the API. ## Skip the next generation [#skip-the-next-generation] ```http POST /v1/recurring_invoices/{recurring_invoice}/skip ``` Advances `next_run_at` by exactly one period **without generating an invoice** for the current cycle. The skipped occurrence is **not** counted against `max_occurrences` — the generated-invoice counter does not move. Use it to jump over a billing period (holidays, a paused client) while keeping the schedule intact. Requires the `recurring_invoices:write` scope. Returns `200` with the recurring-invoice resource (note the advanced `next_run_at`). A cancelled or completed recurrence responds `422`; a recurring invoice that belongs to another company responds `404`. ```bash curl -X POST https://api.factuarea.com/v1/recurring_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/skip \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` <Callout type="info"> Skipping logs a `skipped` entry in the recurring-invoice activity — the history shows the cycle was deliberately omitted, not missed. </Callout> ## Create a recurrence from an existing invoice [#create-a-recurrence-from-an-existing-invoice] ```http POST /v1/invoices/{invoice}/create-recurring ``` Copies the **lines, client and series** of a source invoice into a brand-new recurring invoice (its own UUID v7) and applies the cadence supplied in the body. The source invoice is untouched. Requires the `recurring_invoices:write` scope. | Field | Type | Required | Notes | | ------------------ | --------------------- | -------- | ------------------------------------------------------------------------------ | | `frequency` | string | yes | `daily`, `weekly`, `biweekly`, `monthly`, `quarterly`, `semiannual`, `yearly`. | | `start_on` | string (`YYYY-MM-DD`) | yes | First scheduled run. | | `end_on` | string (`YYYY-MM-DD`) | no | Last allowed run. | | `name` | string | no | Label for the recurrence (≤255). | | `description` | string | no | | | `notes` | string | no | | | `metadata` | object | no | Your key/value pairs. | | `holiday_handling` | string | no | How to shift a run that falls on a holiday. | | `days_before_due` | integer | no | Due date offset for each generated invoice. | | `max_occurrences` | integer | no | Stop after N generated invoices. | | `auto_delivery` | object | no | See [Auto-delivery](#auto-delivery). | Returns `201` with the new recurring invoice and a `Location` header pointing to it. A source invoice that belongs to another company responds `404`. ```bash curl -X POST https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42/create-recurring \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "frequency": "monthly", "start_on": "2026-07-01", "max_occurrences": 12, "auto_delivery": { "send_automatically": true, "recipients": ["billing@acme.example"] } }' ``` ## Auto-delivery [#auto-delivery] The `auto_delivery` object — available on recurring create, update and `create-recurring` — emails every generated invoice automatically. | Field | Type | Notes | | -------------------- | --------- | ----------------------------------------------- | | `send_automatically` | boolean | Master switch. `false` disables sending. | | `recipients` | string\[] | Primary recipients (email). | | `cc` | string\[] | Carbon-copy recipients (email). | | `subject` | string | Custom subject (≤255). `null` uses the default. | | `body` | string | Custom body (≤5000). `null` uses the default. | When `send_automatically` is `true`, each generated invoice is emailed to `recipients` (with optional `cc`) using `subject`/`body`. Setting `send_automatically` to `false` turns delivery off. An empty `recipients` list with `send_automatically: true` is rejected with `422` — there is no one to send to. ```json { "auto_delivery": { "send_automatically": true, "recipients": ["billing@acme.example"], "cc": ["copy@acme.example"], "subject": "Your monthly invoice", "body": "Hi, here is your invoice for this period." } } ``` ## Preview the computed document [#preview-the-computed-document] ```http GET /v1/recurring_invoices/{recurring_invoice}/preview ``` Without `expand`, `preview` returns only the **date forecast** — the upcoming run dates (use `count` to control how many). Pass `expand=document` to **also** compute the next document as a dry-run: the response adds a `next_invoice` block with the resolved `lines` and `totals` (`subtotal`, `tax`, `total`), built from `template_data` **without persisting anything**. Use it to show the customer exactly what the next invoice will contain before it is issued. ```bash curl -G https://api.factuarea.com/v1/recurring_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/preview \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "expand=document" ``` ```json { "data": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "recurring_invoice" }, "next_invoice": { "lines": [ { "description": "Monthly retainer", "quantity": 1, "unit_price": 500, "subtotal": 500 } ], "totals": { "subtotal": 500, "tax": 105, "total": 605 } } } ``` <Callout type="info"> `preview` never creates an invoice. It is a pure read — the dry-run totals are computed in memory from `template_data`. </Callout> ## Per-line fiscal fields [#per-line-fiscal-fields] Each line of `template_data` accepts the fiscal fields VeriFactu and the Spanish tax model need. They flow into every generated invoice. | Field | Type | Notes | | ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `exemption_reason` | string | LIVA exemption / non-subjection cause. One of `E1`–`E6`, `N1`, `N2`. `null` if not informed. | | `regime_key` | string | VeriFactu regime key (`ClaveRegimen`, AEAT list L8.1). `null` if not informed. | | `retention` | number | IRPF withholding percentage (0–100). | | `retention_rate_id` | string (UUID v7) | Catalog tax applied as IRPF withholding. Opaque — does not change the `taxes`/`total` computation (the flat `retention` percentage does). | | `surcharge` | number | Equivalence surcharge percentage (0–100). | | `surcharge_rate_id` | string (UUID v7) | Catalog tax applied as equivalence surcharge. Opaque — the flat `surcharge` percentage drives the maths. | The equivalence surcharge is tied to the line VAT by law. The only legal `tax_rate` → `surcharge` pairs are: | VAT (`tax_rate`) | Surcharge (`surcharge`) | | ---------------- | ----------------------- | | `21` | `5.2` | | `10` | `1.4` | | `4` | `0.5` | Sending an `exemption_reason`, `regime_key`, `retention_rate_id` or `surcharge_rate_id` outside its catalog responds `422` with an `allowed_values` list in the error. An illegal VAT↔surcharge pair (e.g. `21` with `1.4`) is also rejected with `422`. ```json { "template_data": { "lines": [ { "description": "Consulting", "quantity": 1, "unit_price": 1000, "tax_rate": 21, "surcharge": 5.2, "retention": 15, "regime_key": "01", "exemption_reason": null } ] } } ``` --- # Regime keys (/guides/regime-keys) Spanish invoicing overloads the word *regime*. Three distinct concepts share it, they live at different levels of the document, and only one of them is something you send on the public API: | Concept | Level | Set by you in v1? | Drives | | ------------------------------------------------------------------------ | -------------- | ----------------------------------- | ----------------------------------------------------------------------------- | | **Operation regime** — domestic, intra-community, export, reverse charge | Invoice header | **No.** Read-only. | The AEAT operation qualification (`S1`, `S2`, `E5`, `E2`). | | **Regime key** (`ClaveRegimen`, AEAT list L8.1) | Invoice line | **Yes** — `lines[].regime_key` | The special-regime code declared for that line. | | **Indirect tax regime** — VAT, IGIC, IPSI | Invoice line | Yes — `lines[].indirect_tax_regime` | Which tax applies at all. See [Territorial taxes](/guides/territorial-taxes). | Confusing the first two is the single most common cause of a wrongly qualified invoice. This page separates them. ## When this applies [#when] Always: every issued invoice declares a qualification and, for most tax regimes, a regime key. What varies is whether you leave both to derivation or state them explicitly per line. Declare a regime key explicitly when the operation belongs to a special regime — used goods, travel agencies, cash-basis accounting, agriculture, equivalence surcharge, distance sales under the one-stop shop. Header derivation only ever produces the general regime or export, so **the granularity of the full catalogue is reachable only per line**. ## The closed catalogue [#catalog] `lines[].regime_key` accepts exactly these seventeen two-digit codes, from the AEAT `ClaveRegimen` list L8.1 ([`BR-INV-031`](#traceability)). Anything else answers `422` with the full list in `allowed_values`. | Code | Regime | | ---- | ----------------------------------------------------------------------------------------------------- | | `01` | General regime. | | `02` | Export. | | `03` | Used goods, art, antiques and collectors' items (REBU). | | `04` | Investment gold. | | `05` | Travel agencies. | | `06` | VAT group, advanced level. | | `07` | Cash-basis accounting. | | `08` | Operations subject to IPSI or IGIC. | | `09` | Travel-agency services rendered as an intermediary in the name and on behalf of others. | | `10` | Collections on behalf of third parties of professional fees or industrial, author and similar rights. | | `11` | Business-premises leases subject to withholding. | | `14` | VAT not yet accrued — works certifications for a public administration. | | `15` | VAT not yet accrued — successive-supply operations. | | `17` | Operations under Chapter XI of Title IX — one-stop shop (OSS and IOSS). | | `18` | Equivalence surcharge. | | `19` | Agriculture, livestock and fishing (REAGYP). | | `20` | Simplified regime. | The numbers `12`, `13` and `16` are deliberately absent — they are not part of the list, and sending them is rejected like any other value outside the catalogue. ## What the API sends [#api] `regime_key` is an **optional, per-line, additive** field on [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) and [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). A line that omits it falls back to the key derived from the invoice header: ```json { "lines": [ { "description": "Reventa de maquinaria de ocasión", "quantity": 1, "unit_price": 100, "regime_key": "03" }, { "description": "Servicio de instalación", "quantity": 1, "unit_price": 50, "tax_rate": 21 } ] } ``` The first line declares the used-goods regime; the second one, with no key, is derived from the header. Omitting the field on every line reproduces exactly the behaviour that existed before per-line keys were introduced, **fingerprint included** — which is the reason the field is additive rather than mandatory ([`BR-INV-031`](#traceability)). Three of the four published create-invoice examples — `b2c`, `intracomunitario_bienes` and `con_irpf` in the request-body examples dropdown — declare `regime_key: "01"` explicitly rather than relying on the fallback. The fourth, `b2b_nacional`, omits it and lets the header supply the regime, which is equally valid. Copy the explicit habit: a per-line key is self-documenting and survives a change in header derivation. ### The header regime is read-only in v1 [#header-readonly] The invoice object returns `operation_regime`, and neither the create nor the update operation accepts it. **Every invoice created through the public API is born under the general regime.** The document-level exemption cause — `exemption_reason` on the invoice object — is read-only for the same reason. The consequence is concrete and worth stating plainly: the qualification derived from the header will be `S1` for any v1-created invoice, so **exemption and non-subjection must be declared per line**, with `lines[].exemption_reason`. That is exactly what the `intracomunitario_bienes` example does — `tax_rate: 0` plus `exemption_reason: "E5"` — rather than relying on a header regime it cannot set. See [Line tax classification and exemptions](/guides/line-tax-classification-and-exemptions) for the line catalogue, and [Scope and limitations](/guides/scope-and-limitations) for what this boundary does and does not allow. ### The machine-readable catalogue [#tax-catalog] `GET /v1/tax-catalog` (scope `taxes:read`) publishes the fiscal catalogues this page describes — indirect tax regimes with their valid rates and AEAT codes, operation regimes with their legal mentions, exemption causes with their LIVA article, system withholding rates and the legal VAT-to-surcharge pairs — with labels in Spanish, English and Catalan in every response. ```bash curl https://api.factuarea.com/v1/tax-catalog \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Two properties make it safe to cache aggressively ([`BR-TAX-028`](#traceability)): it is **identical for every company** — the query carries no company identifier and none of its sources is scoped to a tenant, so two API keys receive byte-identical bodies and therefore the same `ETag` — and it is derived from the closed value objects in the code rather than from a copied list, so a new case appears automatically instead of silently desynchronising. <Callout type="info"> Withholding rates are published **positive**, whatever sign they are stored with. The block is filtered by "system tax", not by "active": a rate that a company has toggled off is still part of the legal catalogue, and a custom tax created by one tenant never appears in it. </Callout> ## What appears on the PDF [#pdf] The regime key itself is **not printed**. What the reader sees is the legal mention derived from the operation regime — the reference to art. 25 LIVA for an intra-community supply, art. 21 for a third-country operation, art. 84.Uno.2 for reverse charge — and nothing at all for the general regime, which needs no mention ([`BR-TAX-024`](#traceability)). Because those mentions derive from the header regime, and the header regime is not settable in v1, an invoice created through the public API prints no automatic regime mention. Use `notes` if the document needs to state the exemption in prose. ## What reaches the AEAT [#aeat] Two different fields travel per breakdown group, and they answer different questions. **The qualification** answers "what kind of operation is this?", and is derived from the header regime ([`BR-VFC-029`](#traceability)): | Header operation regime | Qualification | What the AEAT receives | | ----------------------- | ------------- | ------------------------------------------------------------------------------- | | General | `S1` | Subject and not exempt, VAT quota `base × rate`. | | Reverse charge | `S2` | Subject and **not** exempt, quota forced to **0** — the recipient self-charges. | | Intra-community | `E5` | Subject and exempt, art. 25 LIVA. | | Import or export | `E2` | Subject and exempt, art. 21 LIVA. | Codes `E1`, `E3`, `E4` and `E6` exist in the AEAT catalogue but are never produced by this derivation: they are only reachable as a **line** exemption reason. A line that declares one wins over the header fallback ([`BR-INV-032`](#traceability)). **The regime key** answers "under which special regime?", and is *not* emitted unconditionally ([`BR-VFC-035`](#traceability)): * Under **IPSI**, the key is never emitted at all. The AEAT validation rules are explicit that this tax carries no `ClaveRegimen`. * Under **VAT** and **IGIC**, the key is derived, with a conservative general default, and never hardcoded to `08`. Code `08` belongs to a mainland issuer whose operation is located in the Canary Islands, Ceuta or Melilla — not to an issuer established there, who declares their own tax with their own list. * A document-level exemption cause that carries its own special-regime key — used goods, agriculture, travel agencies, cash-basis, equivalence surcharge — takes priority over that default. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-INV-031` — the closed L8.1 catalogue for `lines[].regime_key`, its fallback to header derivation, and the identical-fingerprint invariant. * `BR-INV-032` — line-level exemption causes overriding the header-derived qualification. * `BR-VFC-029` — the qualification map from the header operation regime, and the fact that only `E5` and `E2` are reachable that way. * `BR-VFC-035` — how `ClaveRegimen` is derived: never hardcoded `08`, never emitted for IPSI, priority of the exemption cause's special key. * `BR-TAX-024` — the document-level exemption cause and the automatic legal mention. * `BR-TAX-028` — the public fiscal catalogue: its five sources, tenant independence, and the positive publication of withholding rates. --- # Scope and limitations (/guides/scope-and-limitations) Every platform has boundaries. A boundary you can read before you integrate is a design decision; one you discover in production is a defect. This page is the single canonical list — no other guide keeps its own. Each row states the **scenario**, its **status**, and the **workaround**: the alternative available today, or an explicit statement that there is none. There are exactly two statuses, because a third fuzzy category is what turns pages like this into wallpaper: * **By design** — we will not build it. The alternative is here. * **On the roadmap** — deferred, not rejected. <Callout type="info"> Verified on **31 July 2026** against **v1** of the API. A row whose scenario becomes supported is removed in the same change that implements it, rather than left standing as an obsolete limitation. </Callout> ## Limitations verified against the code [#gaps] | Scenario | Status | Workaround | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Self-billing** — the recipient issues the invoice in the supplier's name | By design | Not modelled. The supplier issues their own invoice. If you operate both parties, issue it from the supplier's account. | | **Invoice issued by a third party** | By design | The AEAT third-party-issuer field is not emitted. An agency operating a client's account issues from that account with [`X-Active-Profile`](/guides/acting-on-behalf); the invoice is declared as issued by the company itself. | | **Multi-currency** | By design | The v1 contract exposes euro, fixed: `currency` is always `EUR` and there is no currency column. **Filtering a listing by any other currency returns an empty page, not an error.** Invoice in euro and convert outside Factuarea. | | **TicketBAI / Batuz (Basque Country)** | By design | **No alternative within Factuarea.** The Basque provincial systems use different schemas, certificates and endpoints, and require their own certified software. Companies with a Basque tax domicile are warned during onboarding. | | **Reverse charge, and any header operation regime, declared over the API** | By design | The header `operation_regime` is read-only in v1 — neither create nor update accepts it — so every invoice created through the API is born under the general regime and qualifies `S1`. Exemption and non-subjection are declared per line with `lines[].exemption_reason`, but **reverse charge is qualification `S2` and has no line-level equivalent**: issue those invoices from the dashboard. See [International customers](/guides/international-customers#map). | | **Disbursements outside the issued invoice** — quotes, pro-formas, delivery notes, purchase invoices, recurring templates | By design | Only the issued invoice models disbursements. Include the amount as an ordinary line in the preceding document, and set `line_type` on the resulting invoice **while it is still a draft** — the update operation accepts it. | | **Disbursements in the Facturae and UBL XML** — the amount to pay in the XML is the fiscal total, not the amount due | On the roadmap | Taxable base and tax amounts are correct — the disbursement is properly excluded — but the payable amount falls short by it, and no element of the XML carries the difference. **Do not route an invoice carrying disbursement lines through [FACe](/guides/face-invoicing)** until the native Facturae 3.2.2 block is mapped: invoice the disbursement outside that channel. | | **Disbursements in the aggregate portfolio figures** — the `pending_amount` of [`GET /v1/invoices/stats`](/api-reference/invoices/public-api.v1.invoices.stats), ageing and top-debtor reports | By design | Those aggregates measure **invoiced volume**, the same magnitude the annual third-party return declares, and have never subtracted partial payments either. For the amount actually owed, read `pending_amount` on each invoice, which does measure against the payable amount. | ## Deliberate differences from other platforms [#deliberate] These are conscious product decisions, not gaps. Each one exists because the alternative we chose is better for integrators than the pattern being asked for. | Scenario | Status | Workaround | | ----------------------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Offset pagination** with a page count and jump-to-page-N | By design | Stripe-style **cursor pagination**: `limit` with validated bounds, plus `starting_after` **or** `ending_before` (mutually exclusive). Responses carry `has_more` and `next_cursor`, and `next_cursor` is `null` when `has_more` is false. See [Pagination](/guides/pagination). | | **Permanently dual error envelope** — our envelope and RFC 9457 in the same body, always | By design | **Content negotiation.** `Accept: application/problem+json` returns pure RFC 9457; anything else — `application/json`, `*/*`, no `Accept` header — returns our envelope. See [Errors](/guides/errors). | | **Total atomicity in bulk create** — one bad row rejects the batch | By design | **Partial success.** The response carries `{dry_run, total, successful, failed, results, failures}`, where each failure identifies its row by zero-based `index` with its own error code. Import 480 of 500 and fix the 20. See [Bulk operations](/guides/bulk-operations). | | **Mandatory line totals in the request** (`line_total`, `taxable_base`) | By design | `line_total` is an **optional verified checksum**: compared against the computed total with a one-cent tolerance, then discarded — never persisted, never returned. You do not have to replicate our calculation engine. See [Disbursements](/guides/disbursements#checksum). | | **Representation or power of attorney for third parties** — proxy endpoints, signed authorisation documents | By design | Each company uploads **its own certificate**, which must match its own tax ID, is validated by structure and size, and whose passphrase is stored encrypted. | | **Substituting simplified invoices in two steps** — a corrective plus a new complete invoice | By design | **One native step:** `POST /v1/invoices/substitute-simplified` issues the substitute invoice aggregating several simplified ones. See [Simplified or full invoices](/guides/simplified-vs-full-invoices#substitute). | | **Python SDK** | On the roadmap | Generate a client from the published OpenAPI document, or use the [TypeScript](/sdks/typescript) or [PHP](/sdks/php) SDKs, the [CLI](/cli) or the [MCP server](/mcp). | ## Capabilities you may assume are missing [#capabilities] Four things Factuarea does that integrators arriving from other platforms routinely expect not to find: | Capability | Where | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Substitute invoice for simplified ones, in a single call** — aggregate several tickets into one complete invoice without a corrective first | [Simplified or full invoices](/guides/simplified-vs-full-invoices#substitute) · [`POST /v1/invoices/substitute-simplified`](/api-reference/invoices/public-api.v1.invoices.substitute_simplified) | | **Subsanación of rejected VeriFactu records, exposed in the public API** — repair a refused declaration without annulling the invoice | [VeriFactu record subsanación](/guides/verifactu-subsanacion) · [`POST /v1/verifactu/records/{id}/subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) | | **Corrective by differences with a negative taxable base** — the fiscally correct way to express a refund | [Corrective invoices](/guides/corrective-invoices#nature) | | **AEAT fiscal catalogue queryable over the API** — indirect tax regimes, operation regimes, exemption causes with their LIVA article, withholding rates and the legal VAT-to-surcharge pairs, in three languages | [Regime keys](/guides/regime-keys#tax-catalog) · `GET /v1/tax-catalog` | None of these is announced or in development: all four are live operations today. ## Where the fiscal reasoning lives [#see-also] This page lists boundaries. The guides that explain the rules behind them: <Cards> <Card title="VeriFactu submission states" href="/guides/verifactu-submission-states" description="The record lifecycle, retry and subsanación." /> <Card title="Corrective invoices" href="/guides/corrective-invoices" description="R1–R5, substitution versus differences." /> <Card title="Regime keys" href="/guides/regime-keys" description="Header qualification and the per-line regime catalogue." /> <Card title="Territorial taxes" href="/guides/territorial-taxes" description="VAT, IGIC and IPSI." /> <Card title="Disbursements" href="/guides/disbursements" description="Amounts paid on the customer's behalf." /> <Card title="International customers" href="/guides/international-customers" description="Alternative identification and the scenario map." /> </Cards> ## Traceability [#traceability] This page documents the **absence** of behaviour, which no business rule can assert. Its rows are therefore anchored differently from the other fiscal guides: to a verified point in the code, to the decision recorded for the platform, or — where a business rule does exist — to that rule. **Limitations verified against the code:** | Row | Anchor | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Self-billing | No such capability in the domain. The only occurrences of the concept are the invoice Factuarea issues to its own subscribers and the system-company check — neither is an API capability. | | Invoice issued by a third party | The AEAT third-party-issuer field is never emitted; no occurrence in the application code. | | Multi-currency | `InvoiceV1Resource` returns the literal `'EUR'`, and the v1 read repository documents that any other currency yields an empty page. | | TicketBAI / Batuz | `BR-VFC-019` — deliberately out of scope for the VeriFactu context. | | Reverse charge over the API | No v1 request accepts `operation_regime`; the invoice resource returns it read-only. `BR-VFC-029` derives the qualification from that header regime, and `BR-INV-032` limits the line catalogue to exemption and non-subjection causes, with no `S` codes. | | Disbursements outside the issued invoice | `BR-INV-037` and the line-type value object, which declares that only the issued invoice models disbursements; `BR-INV-040` for the simplified-invoice restriction. | | Disbursements in the Facturae and UBL XML | The warning edge case of `BR-INV-042`, which records that the payable amount of both documents is the fiscal total and that the native Facturae 3.2.2 block is not mapped yet. | | Disbursements in the aggregate portfolio figures | The edge case of `BR-INV-045`, which records that the aggregates measure invoiced volume and are deliberately left measuring it. | **Deliberate differences:** anchored to the shared HTTP components that implement the alternative — cursor pagination, the error content negotiator, the bulk partial-success resource — to `BR-INV-044` for the optional line checksum, to `BR-VFC-003`, `BR-VFC-004`, `BR-VFC-022` and `BR-VFC-024` for company-owned certificates, and to `BR-INV-015` and `BR-INV-016` for the single-step substitution. The Python SDK row reflects a recorded decision to plan it separately once the specification stabilises: it is deferred, not rejected, which is why its status is *On the roadmap* and not *By design*. **Capabilities:** each is anchored to the live route that materialises it — `public-api.v1.invoices.substitute_simplified`, `public-api.v1.verifactu.records.subsanar` and `public-api.v1.tax-catalog.show` — plus `BR-VFC-033` for the negative taxable base and `BR-TAX-028` for the fiscal catalogue. --- # Scopes & irreversibility (/guides/scopes-and-irreversibility) Every public endpoint declares two pieces of safety metadata **in the OpenAPI spec**: the exact scope it enforces and whether the operation can be undone. Clients (the [CLI](/cli/agents), agents, your own tooling) read them to fail fast — block a call when the key lacks the scope, confirm before an irreversible action — instead of discovering the problem from a `403` or an unrecoverable mutation. ## Reading it from the spec [#reading-it-from-the-spec] Each operation in the [OpenAPI spec](/api/openapi) carries two custom extensions: ```json { "operationId": "public-api.v1.invoices.delete", "x-required-scope": "invoices:delete", "x-irreversible": true } ``` * **`x-required-scope`** — the single `resource:action` scope the API key must hold to call the operation. Present on **every** operation. * **`x-irreversible`** — `true` only on operations that cannot be undone. Absent (treated as `false`) on everything else. <Callout type="info"> These are `x-*` vendor extensions, so a generic OpenAPI viewer may not render them — but any client that parses the spec (like the CLI) reads them directly. Generate a client from the spec and you inherit both. </Callout> ## Scopes [#scopes] Scopes are `resource:action` (e.g. `invoices:read`, `clients:delete`) — the same closed catalog the REST API and the API keys use. A request whose key lacks the scope returns `403` with `code: insufficient_scope`. Request only the scopes your integration needs. Time-tracking operations carry their own scopes — `employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read` and `payroll_exports:read` — all gated behind the `control_horario` module. See [Time tracking](/guides/workforce-overview). The full catalog — every scope, what it grants, and the sensitive ones — is in [Scopes & permissions](/mcp/scopes). A scope is **not** always derivable from the resource name: some endpoints enforce a different scope than you'd guess (PDF downloads enforce `pdfs:read`, payment methods enforce `invoices:read`, state transitions enforce a `:transition` scope, VeriFactu actions enforce `verifactu:*`). Always read `x-required-scope` rather than inferring it. <Callout type="info"> An API key may hold the super-scope `*`, which satisfies any `x-required-scope`. See [Authentication](/guides/authentication). </Callout> ## Irreversible operations [#irreversible-operations] An operation marked `x-irreversible: true` has **no undo**: it deletes data, emits a fiscal record, transitions a document to a terminal state, or rotates a secret. The [CLI](/cli/agents#irreversible-operations) asks for a typed confirmation before running one; your own tooling should guard them the same way. These are the categories that carry `x-irreversible: true`: | Category | Examples | Scope | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | **Deletes** (single) | `clients.delete`, `invoices.delete`, `products.delete`, `taxes.delete`, `webhook_endpoints.delete`… | `<resource>:delete` | | **Bulk deletes** | `invoices.bulk_delete`, `clients.bulk_delete`, `products.bulk_delete`, `suppliers.bulk_delete`… | `<resource>:delete` | | **Fiscal emission / numbering** | `invoices.send`, `invoices.mark_sent`, `invoices.assign_real_number`, `invoices.corrective`, `invoices.substitute_simplified` | `invoices:send` / `invoices:write` | | **Void / annul** | `invoices.void`, `invoices.annul` | `invoices:void` | | **Terminal conversions** | `quotes.convert`, `proformas.convert`, `delivery_notes.convert` | `<resource>:transition` | | **Cancel / sign** | `delivery_notes.cancel`, `delivery_notes.sign`, `recurring_invoices.cancel`, `recurring_invoices.generate` | `<resource>:transition` / `:write` | | **VeriFactu (AEAT)** | `invoices.verifactu_create`, `verifactu.records.subsanar`, `verifactu.settings.update`, `verifactu.certificates.revoke` | `verifactu:write` | | **Secret / certificate** | `webhook_endpoints.rotate_secret`, `verifactu.certificates.revoke` | `webhooks:write` / `verifactu:write` | | **GDPR forget** | `delivery_notes.signature_audits.forget` | `delivery_notes:gdpr_forget` | | **FacturaE (B2G) submit** | `invoices.face_submissions.submit`, `face_submissions.cancel` | `facturae:write` | | **Time-record seal** | `monthly_time_record_closes.seal` | `time_entries:write` | <Callout type="warn"> This list is the human-readable summary. The **machine-readable source of truth** is `x-irreversible` in the spec — a client that reads it stays correct even as the catalog grows. </Callout> ## Putting it together [#putting-it-together] A safe client does two checks before a mutation: 1. **Scope-check** — does the key hold `x-required-scope`? If not, stop locally (no wasted round trip). The CLI exits `4`; see [scope-check](/cli/agents#scope-check). 2. **Irreversibility confirm** — is `x-irreversible` true? If so, confirm before calling. The CLI requires `--confirm <id>`; see [irreversible operations](/cli/agents#irreversible-operations). The official [SDKs](/sdks) and the [CLI](/cli) do both for you. If you generate your own client from the spec, wire these two checks yourself from the extensions. --- # Simplified or full invoices (/guides/simplified-vs-full-invoices) Spanish law distinguishes the **complete invoice** (`F1`), which identifies the recipient and lets them deduct VAT, from the **simplified invoice** (`F2`), the ticket a retailer hands over the counter. When a customer later needs a deductible document for a batch of tickets, the law provides a third type: the **substitute invoice** (`F3`), which aggregates several simplified ones. ## When this applies [#when] A simplified invoice is available in retail-style operations below a legal amount. It is **never** available for the cases below, and the eligibility check evaluates them in this exact order — the first one that matches wins: | Blocking condition | `reason_code` | | --------------------------------------- | --------------------------- | | Intra-community operation | `intra_community` | | Reverse charge | `reverse_charge` | | Recipient outside Spain (export) | `export_operation` | | The customer needs a deductible invoice | `client_deduction_required` | | Total above the absolute legal cap | `over_absolute_limit` | The cap that is **enforced in software is 3.000 € including VAT** — the maximum any simplified invoice may reach under RD 1619/2012 art. 4, whatever the sector. Crossing it answers `422` with the reason code `over_absolute_limit` ([`BR-INV-009`](#traceability)). <Callout type="warn"> The general 400 € threshold of the same article is **not** enforced. Factuarea does not ask a company to declare its economic sector, so it cannot know whether the raised limit applies. Staying inside 400 € when your sector does not qualify for the higher figure is the issuer's fiscal responsibility, not something the API will stop you from doing. </Callout> The tax catalogue does not change between the two types. The same VAT rates apply to an `F1` and to an `F2`; what differs is the document's mandatory content, the amount cap and the recipient identification ([`BR-TAX-011`](#traceability)). ## What the API sends [#api] ### Ask before you decide [#eligibility] [`POST /v1/invoices/simplified-eligibility`](/api-reference/invoices/public-api.v1.invoices.simplified_eligibility), scope `invoices:read`. Built for checkout and point-of-sale flows that must choose the document type *before* creating anything. ```bash curl -X POST https://api.factuarea.com/v1/invoices/simplified-eligibility \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{"total": 3400, "client_country": "ES"}' ``` ```json { "data": { "can_be_simplified": false, "must_be_complete": true, "reason_code": "over_absolute_limit", "reason_message": "El importe 3.400,00 EUR supera el límite de 3.000,00 EUR para una factura simplificada.", "sector_limit": 3000 } } ``` `total` is required and is the amount **including VAT**. `client_id`, `client_country`, `is_intra_community`, `is_reverse_charge` and `client_requires_deductible` are optional inputs to the blocking conditions above. ### Creating a simplified invoice is not a v1 operation [#f2-not-in-v1] [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) has no `type` field and no simplified flag, and `client_id` is required. **The public API cannot issue an `F2`.** Every invoice created through v1 is a complete invoice. This is a real boundary, not an omission you can work around with a payload trick. If your point-of-sale issues simplified invoices, they are created through the dashboard or the point-of-sale surface; what v1 gives you over them is the eligibility check, reading them, and the substitution below. The consequence for error handling: the `422` for a disbursement line inside a simplified invoice is unreachable from `POST /v1/invoices` and reachable only through the corrective endpoint on a simplified original — see [Disbursements](/guides/disbursements). ### Substituting simplified invoices, in one call [#substitute] [`POST /v1/invoices/substitute-simplified`](/api-reference/invoices/public-api.v1.invoices.substitute_simplified), scope `invoices:write`. You pass the recipient and the list of simplified invoices to aggregate; you get back a complete `F3`, already issued, with a definitive series number: ```bash curl -X POST https://api.factuarea.com/v1/invoices/substitute-simplified \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" \ -H "Content-Type: application/json" \ -d '{ "client_id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "simplified_invoice_ids": [ "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f601", "0197b1c2-3d4e-7f50-8a61-b2c3d4e5f602" ], "notes": "Consumos de junio" }' ``` Every simplified invoice in the list is validated ([`BR-INV-015`](#traceability)): the list must be non-empty and free of duplicates, each invoice must belong to your company, each must actually be an `F2`, none may be annulled or cancelled, and none may already have a substitute. A failure names the offending invoice number in the `422`. The `F3` is born issued, and its lines are aggregates: one line per substituted invoice, described as the substitution of that invoice number, with quantity one and the original gross total as unit price, and **no tax of its own** — the VAT was already charged on the simplified invoice. <Callout type="info"> Substitution does not annul the originals. Each `F2` keeps its fiscal status and simply records that it has been substituted; the invoice object exposes this as `substituted_by`. An `F3` **can** itself be corrected, like any complete invoice — `R1`–`R4` apply to `F1` and `F3` alike, see [Corrective invoices](/guides/corrective-invoices). What an `F3` cannot be is *substituted*: only an `F2` may be the target of a substitution, and only an `F3` may carry substituted invoices at all ([`BR-INV-016`](#traceability)). </Callout> ## What appears on the PDF [#pdf] The visible difference is the recipient block. A complete invoice prints the recipient's name, tax ID and address, frozen at issue time; a simplified one may legitimately have none, and prints the final-consumer placeholder instead ([`BR-INV-024`](#traceability)). The `F3` prints as an ordinary complete invoice — a full recipient block and one line per substituted ticket, naming each substituted invoice number. ## What reaches the AEAT [#aeat] The invoice type travels as the AEAT `TipoFactura` in the VeriFactu record and is visible on the record object as `invoice_type`: `F1`, `F2`, `F3`, or `R5` for a corrective of a simplified one. The record also flags whether it substitutes simplified invoices ([`BR-VFC-014`](#traceability)). The type also has consequences in the periodic returns ([`BR-TXR-004`](#traceability)): * An `F3` with no recipient tax ID raises a **non-blocking** warning in the quarterly VAT return: unusual, but legitimate if the original ticket had none either. * An `F2` with a registered customer but no tax ID raises a warning too. * An `F2` with no tax ID at all is **excluded from the annual third-party operations return** (**Modelo 347**) by AEAT rule, and the exclusion is reported as a warning. None of these block generation. The report is produced and the warnings are returned alongside it, as an empty list when there are none. Blocking would be a frequent false positive. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-INV-009` — the enforced 3.000 € cap, the unenforced 400 € threshold, and the operations that disqualify a simplified invoice. * `BR-INV-015` — substitution of simplified invoices by an `F3`, its validations and its aggregated lines. * `BR-INV-016` — only an `F3` carries substituted invoices, and never an empty list. * `BR-INV-024` — the immutable recipient snapshot, and its absence for a final-consumer ticket. * `BR-TAX-011` — the tax catalogue is identical for both types; the limit belongs to invoicing, not to the catalogue. * `BR-VFC-014` — the AEAT invoice types and how the type is resolved. * `BR-TXR-004` — non-blocking fiscal-quality warnings for `F2` and `F3` without a tax ID. The `422` reason codes quoted in [When this applies](#when) come from the eligibility domain service that materialises `BR-INV-009`. --- # Tags & custom fields (/guides/tags-and-custom-fields) Two cross-cutting fields let you classify and enrich documents with your own business data: **`tags`** (free classification labels you can filter lists by) and **`custom_fields`** (an ordered list of typed `{field, value}` pairs). Both are set on create/update and returned on every read. | Field | Shape | Limits | Filterable | Resources | | --------------- | --------------------------------- | --------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------- | | `tags` | array of slugs | ≤ 30, each ≤ 40 chars | **Yes** (`?tags=`, `?tags[in]=`) | invoices, quotes, proformas, delivery\_notes, purchase\_invoices, recurring\_invoices, products | | `custom_fields` | ordered array of `{field, value}` | ≤ 50 entries | No | invoices, quotes, proformas, delivery\_notes, purchase\_invoices, recurring\_invoices | ## Tags [#tags] A tag is a **lowercase slug** matching `[a-z0-9-]` — letters, digits and hyphens only. Each tag is at most **40 characters**, and a document carries at most **30 tags**. Pass them as a JSON array of strings on create or update: ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "tags": ["consultoria", "cliente-vip"], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` Tags are returned as a plain array on every read (empty `[]` when there are none): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "number": "F-2026-0042", "tags": ["consultoria", "cliente-vip"] } ``` <Callout type="warn"> `tags` is a **full replacement** on update: sending `"tags": ["a"]` replaces the whole set, it does not append. To add a tag, send the full list including the existing ones. To clear them, send `[]`. </Callout> ### Filtering by tag [#filtering-by-tag] List endpoints accept two query parameters to filter by tag — pick one: | Parameter | Semantics | Example | | ---------- | ----------------------------------------------------------------------------------------- | ------------------------------ | | `tags` | **Exact match** on a single slug. | `?tags=cliente-vip` | | `tags[in]` | Comma-separated list, **OR** semantics — matches documents carrying **any** of the slugs. | `?tags[in]=cliente-vip,moroso` | ```bash # All invoices tagged "cliente-vip" curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags=cliente-vip" # Invoices tagged "cliente-vip" OR "moroso" curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags[in]=cliente-vip,moroso" ``` The tag filter is available on `invoices`, `quotes`, `proformas`, `delivery_notes`, `purchase_invoices` and `recurring_invoices`. It combines with the other filters and with [cursor pagination](/guides/pagination). ## Custom fields [#custom-fields] `custom_fields` is an **ordered array** of typed `{field, value}` pairs, for business data you want to display alongside the document (cost centre, purchase order number, project code…). Up to **50** entries; each `field` is a non-empty string of at most **60 characters** and each `value` is a string of at most **500 characters**. Order is preserved exactly as you send it. ```bash curl -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "custom_fields": [ { "field": "centro_coste", "value": "CC-2026-001" }, { "field": "numero_pedido", "value": "PO-2026-0042" } ], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' ``` Like `tags`, the whole array is a full replacement on update, and it is returned in order on every read (empty `[]` when there are none). ## Custom fields vs metadata [#custom-fields-vs-metadata] Both `custom_fields` and `metadata` carry your own data, but they serve different purposes — don't reach for the wrong one. <Callout type="info"> Use **`custom_fields`** for ordered, typed business data the user sees on the document. Use **`metadata`** for an unordered key→value map of opaque integration data (ERP codes, your own ledger references) that nobody reads visually. A document may carry **both**. </Callout> | | `custom_fields` | `metadata` | | --------- | ------------------------------------ | ------------------------------- | | Shape | Ordered **list** of `{field, value}` | Unordered **map** `key → value` | | Order | Preserved | None | | Limit | ≤ 50 entries | ≤ 50 keys | | Key | `field`, 1–60 chars | map key | | Value | string ≤ 500 chars | string ≤ 500 chars | | Intent | Business fields the user sees | Opaque integration data | | Resources | The six document resources | All resources | The master resources (`clients`, `suppliers`) have no typed `custom_fields` — use their `metadata` as the untyped custom-fields store. `products` accept `tags` but no `custom_fields`. ## Examples [#examples] <Tabs items="['Python', 'Node.js', 'Bash (curl + jq)']"> <Tab value="Python"> ```python import os, requests base = 'https://api.factuarea.com/v1' headers = {'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"} # Create an invoice with tags + custom_fields resp = requests.post(f'{base}/invoices', headers=headers, json={ 'client_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01', 'series_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02', 'issued_on': '2026-05-15', 'due_on': '2026-06-15', 'tags': ['consultoria', 'cliente-vip'], 'custom_fields': [ {'field': 'centro_coste', 'value': 'CC-2026-001'}, {'field': 'numero_pedido', 'value': 'PO-2026-0042'}, ], 'lines': [ {'description': 'Monthly service', 'quantity': 1, 'unit_price': 99.00, 'tax_rate_id': '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03'}, ], }) resp.raise_for_status() # List invoices tagged "cliente-vip" OR "moroso" rows = requests.get(f'{base}/invoices', headers=headers, params={'tags[in]': 'cliente-vip,moroso'}).json()['data'] print(len(rows), 'matching invoices') ``` </Tab> <Tab value="Node.js"> ```javascript const base = 'https://api.factuarea.com/v1'; const headers = { Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`, 'Content-Type': 'application/json', }; // Create an invoice with tags + custom_fields await fetch(`${base}/invoices`, { method: 'POST', headers, body: JSON.stringify({ client_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01', series_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02', issued_on: '2026-05-15', due_on: '2026-06-15', tags: ['consultoria', 'cliente-vip'], custom_fields: [ { field: 'centro_coste', value: 'CC-2026-001' }, { field: 'numero_pedido', value: 'PO-2026-0042' }, ], lines: [ { description: 'Monthly service', quantity: 1, unit_price: 99.0, tax_rate_id: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03' }, ], }), }); // List invoices tagged "cliente-vip" OR "moroso" const url = new URL(`${base}/invoices`); url.searchParams.set('tags[in]', 'cliente-vip,moroso'); const { data } = await fetch(url, { headers }).then((r) => r.json()); console.log(data.length, 'matching invoices'); ``` </Tab> <Tab value="Bash (curl + jq)"> ```bash # Create an invoice with tags + custom_fields curl -s -X POST https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01", "series_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a02", "issued_on": "2026-05-15", "due_on": "2026-06-15", "tags": ["consultoria", "cliente-vip"], "custom_fields": [ { "field": "centro_coste", "value": "CC-2026-001" }, { "field": "numero_pedido", "value": "PO-2026-0042" } ], "lines": [ { "description": "Monthly service", "quantity": 1, "unit_price": 99.00, "tax_rate_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } ] }' | jq '{id, tags, custom_fields}' # List invoices tagged "cliente-vip" OR "moroso" curl -s -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "tags[in]=cliente-vip,moroso" | jq '.data | length' ``` </Tab> </Tabs> --- # Territorial taxes — VAT, IGIC and IPSI (/guides/territorial-taxes) Mainland Spain and the Balearic Islands charge **VAT**. The Canary Islands charge **IGIC**. Ceuta and Melilla charge **IPSI**. They are three different taxes with three different rate grids, three different AEAT codes and three different administrations — and treating them as one is how a Canary Islands company ends up over-declaring its VAT. ## When this applies [#when] The applicable regime derives from the AEAT zone of the issuing company: mainland → VAT, Canary Islands → IGIC, Ceuta and Melilla → IPSI ([`BR-TAX-020`](#traceability), art. 1.3 RRSIF). Derivation is the **fallback**, not the whole story. Since operations can be located somewhere other than where the issuer is established, a document may override the regime explicitly ([`BR-TAX-027`](#traceability)) — see [Choosing the regime](#override). ## The three rate grids [#rates] Each regime has a closed grid of legal rates and a general rate ([`BR-TAX-020`](#traceability)): | Regime | AEAT code | Legal rates | General rate | | ------ | --------- | ----------------------- | ------------ | | VAT | `01` | 0, 4, 10, 21 | 21% | | IPSI | `02` | 0, 0.5, 1, 2, 4, 8, 10 | 8% | | IGIC | `03` | 0, 3, 5, 7, 9.5, 15, 20 | 7% | 0% is valid in all three — it represents the exempt operation. <Callout type="warn"> Note the AEAT codes: **IPSI is `02` and IGIC is `03`**, not the other way round. An earlier internal value object had them inverted; emitting the wrong one produces a rejection or a wrong declaration. </Callout> The grid is **enforced when creating or editing an IGIC or IPSI tax**: a rate outside its regime's grid answers `422` listing the legal rates ([`BR-TAX-021`](#traceability)). Moving an existing tax to another zone re-validates its rate against the new regime, so a 21% tax cannot be relabelled as Canary Islands without changing the rate first. VAT is deliberately **not** narrowed this way. The seeded catalogue contains historical and transitional rates — 2%, 5%, 7.5% from the anti-inflation measures — that do not belong to a closed grid, and rejecting them would break existing data. ## What the API sends [#api] ### Finding the right taxes [#catalog] [`GET /v1/taxes`](/api-reference/taxes/public-api.v1.taxes.list) accepts both `country_aeat_zone` (`peninsula`, `canarias`, `ceuta`, `melilla`) and the derived `indirect_tax_regime` (`iva`, `igic`, `ipsi`). The two are equivalent views of the same dimension: `?indirect_tax_regime=igic` matches `?country_aeat_zone=canarias` ([`BR-TAX-026`](#traceability)). ```bash curl "https://api.factuarea.com/v1/taxes?indirect_tax_regime=igic" \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Each tax exposes its `indirect_tax_regime`, derived read-only from its zone, and `linked_surcharge_taxes_id`, the equivalence surcharge legally paired with it. An unknown regime in the filter degenerates to an empty list — it never invents results. Taxes that are not consumption taxes — withholdings, surcharges — carry a null regime. ### Choosing the regime for a document [#override] `lines[].indirect_tax_regime` accepts `iva`, `igic` or `ipsi` on [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) and [`PUT /v1/invoices/{id}`](/api-reference/invoices/public-api.v1.invoices.update). Precedence is **override over zone** ([`BR-TAX-027`](#traceability)): <Steps> <Step> A valid override **wins** over the regime derived from the tax's zone. </Step> <Step> No override → the regime is derived from the zone, the historical behaviour. </Step> <Step> Neither a resolvable tax nor an override → the snapshot stays empty. Nothing is inferred. </Step> </Steps> Two guards apply, both `422`: * A value outside `iva|igic|ipsi` is rejected as a **domain** invariant, not just a form-level one — the same answer whichever surface sends it. * **All lines of a document must share the same regime.** The override is document-wide, not per line; mixing two regimes answers `422` with a business-rule violation. Lines with no regime are ignored by the check, so a document combining explicit `igic` lines with untyped lines is homogeneous. ```json { "lines": [ { "description": "Servicio prestado en Canarias", "quantity": 1, "unit_price": 100, "indirect_tax_regime": "igic" }, { "description": "Materiales", "quantity": 2, "unit_price": 50, "indirect_tax_regime": "igic" } ] } ``` The chosen regime is part of the line's **immutable fiscal snapshot**, so it **survives conversion**: a quote or pro-forma converted into an invoice inherits the regime that was chosen, rather than recomputing it from today's company zone ([`BR-TAX-023`](#traceability)). The companion field `aeat_tax_code` is always derived from the tax and is never overridable; an incoming value is ignored. ## What appears on the PDF [#pdf] The tax column and the totals block show the rates that were actually applied, so an IGIC invoice prints IGIC rates. The regime name itself is not a separate printed element; it is visible through the rates and, when the document carries one, the legal mention of its exemption cause ([`BR-TAX-024`](#traceability)). Because the snapshot is immutable, a company that later moves its registered address does not retroactively change the documents it has already issued. ## What reaches the AEAT [#aeat] **In the VeriFactu record**, the `Impuesto` field of each breakdown group is derived from the line's regime — `01` for VAT, `02` for IPSI, `03` for IGIC — and is never hardcoded ([`BR-VFC-034`](#traceability)). A mixed invoice produces one breakdown group per (rate, regime) pair, and the fingerprint seals the set. Historical lines with no regime snapshot keep `01`, so the XML of invoices already declared is not altered. The regime key travels differently: under IPSI it is **not emitted at all**, and under VAT and IGIC it is derived rather than hardcoded — see [Regime keys](/guides/regime-keys#aeat). **In the quarterly VAT return**, the rule is absolute: [`POST /v1/tax_reports/303`](/api-reference/tax-reports/public-api.v1.tax_reports.generate_303) aggregates **only** lines whose regime is VAT ([`BR-TXR-039`](#traceability), [`BR-TXR-020`](#traceability)): * **Output tax.** A line of any other regime is skipped. It never reaches a VAT box. * **Input tax.** The base and quota of non-VAT lines are subtracted from the invoice total, so a purely VAT purchase keeps exactly its previous amount, and a mixed purchase contributes only its VAT part. IGIC and IPSI borne are **not deductible** in this return — they are different taxes. IGIC is settled with the Canary Islands tax agency; IPSI with the local administration of Ceuta or Melilla. Neither has anything to do with the state VAT return. Snapshot lines are grouped by the composite key **(regime, rate)** rather than by rate alone, which is what prevents an IPSI line at 10% from merging with a VAT line at 10% ([`BR-TXR-038`](#traceability)). Among VAT lines, every rate present is emitted — including 2%, 5% and 7.5% — so nothing is lost from the AEAT file because it falls outside the usual three. ### The territorial notice is a warning, never a block [#warning] A company in a special territory that generates its VAT return receives a **Spanish-language warning** in the `warnings` list, naming the administration before which the indirect tax is settled. The file is still produced, with the VAT part only ([`BR-TXR-040`](#traceability)). It is deliberately not a `422`. A Canary Islands company can have perfectly legitimate VAT — sales to the mainland, for instance — and blocking would deny it a valid return. The warning appears only when the zone is special **and** the period actually contains indirect-tax operations, and it accumulates with the other fiscal-quality warnings. The annual third-party operations return (**Modelo 347**) behaves differently: it **does** include IGIC and IPSI operations at their total amount including tax, because it is agnostic to which indirect tax applies. See [Disbursements](/guides/disbursements#aeat) for what changes its base. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-TAX-020` — the indirect tax regime as a value object derived from the AEAT zone, its legal rate grids and its AEAT codes. * `BR-TAX-021` — rate-grid enforcement when creating or editing an IGIC or IPSI tax, and why VAT is not narrowed. * `BR-TAX-023` — the immutable per-line fiscal snapshot. * `BR-TAX-024` — the document-level exemption cause and its legal mention. * `BR-TAX-026` — catalogue filtering by zone and regime, and the exposed linked surcharge. * `BR-TAX-027` — the per-document regime override, its precedence, its two `422` guards and its survival through conversion. * `BR-VFC-034` — the breakdown `Impuesto` derived per line, never hardcoded. * `BR-TXR-020` — the VAT return aggregates only VAT lines, and loses no VAT rate. * `BR-TXR-038` — the snapshot groups by (regime, rate). * `BR-TXR-039` — exclusion of IGIC and IPSI from both output and input VAT. * `BR-TXR-040` — the territorial warning that never blocks generation. --- # Test mode & sandbox (/guides/test-mode) Every Factuarea API key belongs to one of two **environments**, told apart by its prefix: | Prefix | Environment | Operates on | External effects | | ------------ | --------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------ | | `fact_live_` | **live** (production) | Your real company | Real: legal fiscal numbering, VeriFactu → AEAT, FACe submissions, emails to clients, outbound webhooks | | `fact_test_` | **test** (sandbox) | An isolated *sandbox* company | **Switched off** (see below) | The prefix is the **source of truth** for the environment: a `fact_test_` key always operates in test mode and a `fact_live_` key always in live. No request parameter changes the environment — it is determined entirely by the key you authenticate with. <Callout type="info"> Always build and test your integration with a `fact_test_` key first. Once your flow works end-to-end, switch the prefix to `fact_live_` to go to production. The API surface is **identical** in both environments. </Callout> ## Getting a test key [#getting-a-test-key] Test keys are created from the developer dashboard exactly like live keys, selecting the **Test** environment ([app.factuarea.com/settings/developers/api-keys](https://app.factuarea.com/settings/developers/api-keys)). The generated secret looks like: ``` fact_test_<24 alphanumeric characters> ``` Example: ``` fact_test_3pXnR2VbY7TcA9eFmN5z8KqW ``` Same format and entropy as a live key (24 base62 characters), same scopes, same rate-limit tier. The only difference is the prefix and what it points to. As with live keys, the secret is shown **only once** at creation — if you lose it you must rotate. ## Using a test key [#using-a-test-key] Send it on every request just like a live key, via `Authorization: Bearer` or `X-API-Key`: ```bash curl https://api.factuarea.com/v1/clients \ -H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" ``` The same endpoints and operations available in live are available in test — nothing is removed or stubbed. ## Data isolation: the sandbox company [#data-isolation-the-sandbox-company] A `fact_test_` key operates on a dedicated **sandbox company** — a technical "twin" of your real company, provisioned automatically the first time you use test mode, that inherits your real company's plan so feature-gating is faithful. Thanks to multi-tenant isolation by company: * Resources created with a `fact_test_` key are **not visible** to a `fact_live_` key, and vice versa. * Test fiscal numbering uses the sandbox's own series and **never** consumes or alters the correlative numbering of your production series. This is structural isolation, not a filter: test and live data live in separate companies, so there is no way for them to mix. ## What is switched off in test [#what-is-switched-off-in-test] When you operate with a `fact_test_` key (sandbox environment), effects that reach the outside world are **disabled** so you can exercise your integration without real-world consequences: | Effect | In `live` | In `test` | | ------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **VeriFactu** | The Alta record is created and transmitted to the AEAT. | The Alta record is created **locally**, but **never transmitted to the AEAT**. | | **Email** | Document emails are delivered to real recipients. | Document emails are **not delivered** to real recipients. | | **Webhooks** | Subscribed events are delivered to your external HTTP endpoints. | Events are recorded with `livemode: false` (queryable via `GET /v1/events`) but **not delivered** to your endpoints. | | **FACe (FacturaE)** | Submissions are presented to the real FACe web service. | The whole flow is **simulated** — no SOAP call leaves Factuarea and the registry number is synthetic (`FACE-SANDBOX-*`). See [FACe invoicing](/guides/face-invoicing#sandbox). | Everything else behaves identically: validation, totals, document state machines, idempotency, pagination, rate limits and error envelopes are the same as in production. <Callout type="warn"> Because webhooks are not delivered in test, you cannot exercise your webhook receiver against sandbox data. Test your endpoint's signature verification with the dedicated `POST /v1/webhook_endpoints/{id}/ping` (which is delivered) or against a live key on a controlled event. </Callout> ## With the official SDKs [#with-the-official-sdks] The [TypeScript and PHP SDKs](/sdks) follow the same rule: **the key prefix selects the environment** — there is no flag. Build against a `fact_test_` key, then swap the env var to `fact_live_` to go to production. No code changes. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const sandbox = new Factuarea({ apiKey: "fact_test_…" }); sandbox.environment; // "test" const prod = new Factuarea({ apiKey: "fact_live_…" }); prod.environment; // "live" ``` The SDK exposes the resolved environment on `.environment`, derived from the prefix — handy for guards and logging. </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\FactuareaClient; $sandbox = FactuareaClient::create('fact_test_…'); // sandbox $prod = FactuareaClient::create('fact_live_…'); // production ``` </Tab> </Tabs> <Callout type="warn"> Webhooks are still **not delivered** in test even via the SDK. To exercise your receiver's [SDK verifier](/sdks#verifying-webhooks) in sandbox, use `POST /v1/webhook_endpoints/{id}/ping`, which *is* delivered. </Callout> ## Switching environment in the app [#switching-environment-in-the-app] Beyond API keys, the Factuarea web app lets you toggle between **live** and **test** at any time from the top bar. Switching to test: * Re-issues your session **without re-login**, pointing it at the sandbox company (provisioning it if it doesn't exist yet). The previous token is invalidated. * Shows a persistent **"MODO TEST"** banner across the whole interface so the active context is always obvious. * Re-hydrates the client state so listings reflect sandbox data and never show cached production data. Switching back to live re-issues the session against your real company. The sandbox is never shown as a real company in the company selector — it exists only to back the test environment. ## From test to production [#from-test-to-production] When your integration works against `fact_test_`: 1. Create a `fact_live_` key in the dashboard (same scopes you validated in test). 2. Swap the key your client uses (environment variable / secret manager). 3. No code changes are needed — the request shape is identical. From that point, real effects (fiscal numbering, VeriFactu → AEAT, emails, webhooks) are active again. --- # Time clock (/guides/time-clock) Clocking writes to an **append-only ledger**: clocking in, pausing, resuming and clocking out each append a new entry that is never edited or deleted. Every entry is chained to the previous one with a **SHA-256 hash** ([per-company chain](/guides/workforce-overview)), so any tampering is detectable. The **live session state** —`not_started`, `working`, `paused`, `finished`— is **derived** from the ledger, not stored in a column. All endpoints live under `https://api.factuarea.com/v1` and use the `time_entries:read` / `time_entries:write` scopes, the same [error envelope](/guides/errors) and [cursor pagination](/guides/pagination) as the rest of the API. ## Clock in, pause, resume, clock out [#clocking] Four write operations drive a working day. Each takes an optional `occurred_at` (defaults to now) and a `source`, and returns the appended entry. | Operation | Endpoint | From state | | ------------- | --------------------------------- | --------------------------- | | Clock in | `POST /v1/time-entries/clock-in` | `not_started` or `finished` | | Start a pause | `POST /v1/time-entries/pause` | `working` | | Resume | `POST /v1/time-entries/resume` | `paused` | | Clock out | `POST /v1/time-entries/clock-out` | `working` or `paused` | Two rules govern the sequence. **One open day at a time**: clocking in twice returns `422` ("Ya has fichado la entrada."); pausing or clocking out with no open day returns `422`. **Monotonic chronology**: an `occurred_at` earlier than the last event of the shift is rejected with `422`. A day can have **several shifts** (jornada partida) — clocking in again after clocking out opens a brand-new shift. ```bash curl -X POST https://api.factuarea.com/v1/time-entries/clock-in \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "web" }' ``` Read the current open session with `GET /v1/time-entries/current`, list entries with `GET /v1/time-entries` and fetch one with `GET /v1/time-entries/{time_entry}` — all under `time_entries:read`. See the schemas in the [API Reference](/api-reference/time-entries/public-api.v1.time_entries.clock_in). ## Retroactive (manual) entries [#manual] `POST /v1/time-entries/manual` records a **complete past shift** (clock-in, optional pauses and clock-out) for an employee who forgot to clock. A `reason` is **required** — it is sealed into the hash chain as part of the evidence — and the entry is flagged `is_retroactive` with `source: manual`. Unlike live self-service clocking, a manual entry is a privileged action and is written to the audit log. ```bash curl -X POST https://api.factuarea.com/v1/time-entries/manual \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "started_at": "2026-02-03T09:00:00+01:00", "ended_at": "2026-02-03T17:00:00+01:00", "reason": "Forgot to clock in; confirmed by manager" }' ``` ## The correction workflow [#corrections] A time record is **never** edited. To fix a mistake, an employee opens a **correction request**; a manager or admin then approves or rejects it. A request moves `pending → approved` or `pending → rejected`, both terminal. | Operation | Endpoint | Effect | | -------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Request a correction | `POST /v1/time-corrections` | Creates a `pending` request. | | Approve | `POST /v1/time-corrections/{correction}/approve` | Appends a correction entry to the original; emits `time_entry.corrected`. | | Reject | `POST /v1/time-corrections/{correction}/reject` | Records a rejection with a reason; the original is untouched. | | List / show | `GET /v1/time-corrections`, `GET /v1/time-corrections/{correction}` | Read the workflow state. | Approval **appends a new entry** that references the original one — the mistake and its fix both stay in the ledger. Two guards apply: you **cannot approve your own** request (`422`), and a request already resolved cannot be resolved again (`422`). ```bash curl -X POST https://api.factuarea.com/v1/time-corrections/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/approve \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "note": "Verified against the access log" }' ``` <Callout type="warn"> There is no update or delete for a time record. Every correction is a **new entry** that keeps the original intact — that is what makes the register defensible before the Labour Inspectorate. </Callout> ## Verify chain integrity [#chain] `GET /v1/time-entries/chain/validate` recomputes the whole hash chain and reports whether it is intact, returning the id of the first broken entry if any. It is a **read-only** integrity check (rate-limited) — use it to prove the register has not been altered. ## Next steps [#next] * [Work schedules](/guides/work-schedules) — the expected hours the ledger is measured against. * [Monthly close](/guides/monthly-time-close) — freeze and seal a finished month. * Explore the [time-entries](/api-reference/time-entries/public-api.v1.time_entries.list) and [time-corrections](/api-reference/time-corrections/public-api.v1.time_corrections.create) reference. --- # VeriFactu auto-submission (/guides/verifactu-auto-submission) Integrators arriving from other invoicing platforms look for the operation that submits an invoice to the tax authority, do not find it, and assume the feature is missing. It is not missing: **submission is not a step you perform.** The registration is created as a consequence of issuing the invoice, and transmitted by a background pipeline. This page is the answer to "why hasn't my invoice reached the AEAT?", which is almost always one of the gates below rather than a failure. ## When this applies [#when] To every invoice that leaves `draft` for a company whose VeriFactu activation is effective. Concretely, the registration is created on the transition into `sent` — including invoices that are born already issued: correctives, `F3` substitutes, recurring generations, and creations that pass `status: sent` directly. The `draft` stage is deliberately outside the mechanism. A draft has no definitive number, no frozen recipient snapshot and no fiscal existence; nothing is declared for it. ## The gates, in the order they are evaluated [#gates] <Steps> <Step> **Instance kill-switch.** A global flag can disable VeriFactu for the whole installation. It is an emergency switch, never an activation: on its own it enables nothing. </Step> <Step> **Per-company activation.** This is the one you control. It defaults to **off** for a newly created account — a fresh company does *not* register invoices until someone activates VeriFactu. Effective activation is `instance AND company` ([`BR-VFC-025`](#traceability)). Read it with [`GET /v1/verifactu/config`](/api-reference/verifactu/public-api.v1.verifactu.config): the `enabled` field is already the effective value, not the raw company flag. </Step> <Step> **Operating mode.** With activation on, a company still chooses between transmitting and not transmitting. In `no_verifactu` mode the chained records are still generated and stored locally — the mode changes transmission, not chaining — and must be available for inspection, but nothing is sent in real time ([`BR-VFC-018`](#traceability), RD 1007/2023 art. 16). </Step> <Step> **Historical-import bypass.** Invoices loaded through the bulk import of pre-adhesion history carry a transient flag that makes the VeriFactu handlers return without creating any record ([`BR-INV-011`](#traceability), [`BR-VFC-009`](#traceability)). Without it, importing years of history would declare thousands of registrations with issue dates from before the company joined the system. The flag is forced by the importer and is **not** exposed on the ordinary creation endpoints — you cannot set it from the public API. </Step> <Step> **Active certificate.** Signing needs the company's own FNMT certificate. If there is none, or it is expired, revoked, or its tax ID does not match the company's, creation of the registration fails with a business-rule error. Check `has_active_certificate` on the config endpoint before going live. </Step> </Steps> If all five pass, the record is created, chained, and queued for transmission. Whether the queue transmits automatically is itself an instance-level setting surfaced read-only as `auto_transmit` on the config endpoint. ## What the API sends [#api] Nothing you write. There is no request body for "submit", and no field on [`POST /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.create) that controls it. What you do control is *when the invoice is issued*, and everything follows from that: * Create the invoice as a draft, then issue it with [`POST /v1/invoices/{id}/send`](/api-reference/invoices/public-api.v1.invoices.send) or [`POST /v1/invoices/{id}/mark-sent`](/api-reference/invoices/public-api.v1.invoices.mark_sent). * Or create and issue atomically by passing `options.issue_directly` on the create call. Because the "create and issue in one call" path emits both a creation and an issue event, two handlers race to create the same registration. The command is **idempotent by invoice**: the second one detects the existing registration and silently no-ops, so exactly one record exists per invoice ([`BR-VFC-008`](#traceability)). You do not need to de-duplicate on your side. ### The one explicit escape hatch [#force] There *is* an operation that forces the creation of a registration for an already-issued invoice: [`POST /v1/invoices/{id}/verifactu`](/api-reference/verifactu/public-api.v1.invoices.verifactu_create), scope `verifactu:write`. It creates the registration and enqueues its transmission, answering `201` with the new record. It exists for the case where an invoice was issued while a gate was closed — a certificate that had not been uploaded yet, for instance — and you want the registration once the gate opens. It is **not** a re-send: | Situation | Answer | | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | | The invoice already has a registration | `422` `verifactu_already_submitted` | | The invoice is still a draft, VeriFactu is disabled for the instance, or the certificate is missing, expired, revoked or has a mismatched tax ID | `422` `verifactu_not_eligible` | | The invoice does not exist, or belongs to another company | `404` `invoice_not_found` | ```bash curl -X POST https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ### The only manual levers on a record that already exists [#levers] Once the registration exists, exactly two operations act on it, and both are covered in [VeriFactu submission states](/guides/verifactu-submission-states): * [`retry`](/api-reference/verifactu/public-api.v1.verifactu.records.retry) — re-sends the stored declaration unchanged, for technical failures. * [`subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) — regenerates the declaration from corrected master data, for AEAT rejections. There is no operation that re-transmits an accepted record. Acceptance is terminal by regulation. ## Activation is a commitment, not a toggle [#commitment] Turning VeriFactu on is asymmetric, and an integration that treats it as a reversible switch will hit a `422` in production. Moving to the verifiable mode is always allowed. Moving **back** is blocked until 31 December of the year in which it was activated ([`BR-VFC-001`](#traceability), [`BR-VFC-023`](#traceability), RD 1007/2023 art. 13). The integrity of a chain declared to the AEAT in real time cannot be downgraded to self-certified software halfway through a tax year. There is one deliberate escape: while the chain is still **empty** — the company has not emitted a single billing record in any state — the company may change its mind and go back, and the lock is cleared. The first record emitted, even a rejected or errored one, arms the lock until the end of the year. Turning off the per-company activation flag is blocked by the same guard, so it cannot be used to sidestep the commitment. `GET /v1/verifactu/config` exposes `is_locked_until` so you can show this to your users before they commit. ## Sandbox and production are not interchangeable [#environments] Each company operates against one AEAT environment, exposed as `environment` on both the config object and every record. A CSV obtained against the AEAT test bed is **not** a registration: test CSVs carry a recognisable prefix, and a production database containing them means something was simulated that should have been transmitted ([`BR-VFC-017`](#traceability)). The rule that protects you is that the system must never fall back to simulation silently — an unreachable endpoint has to surface as a technical `error` state, not as a fabricated acceptance. When you reconcile, treat the `environment` field as part of the record's identity. ## What appears on the PDF [#pdf] Auto-submission itself adds nothing to the document; the printed artefact depends on the *existence* of a record, not on how it was created. Once a record exists, the invoice carries the legal QR block ([`BR-VFC-015`](#traceability)), and the legend under the code differs between the two operating modes: the short `VERI*FACTU` mark in verifiable mode, and the full sentence stating the invoice is verifiable at the AEAT electronic office in the other. An invoice imported with the historical bypass has no record and therefore prints **no QR**. That is correct: pre-adhesion invoices are not verifiable at the AEAT. ## What reaches the AEAT [#aeat] One registration declaration per issued invoice, chained to the company's previous record, plus an annulment declaration if the invoice is later annulled (see [Annul or correct](/guides/annul-vs-correct)). Nothing else is transmitted as a consequence of issuing. In `no_verifactu` mode nothing reaches the AEAT in real time at all; the company keeps the local chain for inspection and the system periodically records summaries of its own operational events, which the regulation treats as separate evidence ([`BR-VFC-018`](#traceability)). ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-VFC-001` — adhesion to the verifiable mode is irrevocable until the end of the calendar year, with the empty-chain exception. * `BR-VFC-008` — idempotency: one registration per invoice, even when the create-and-issue flow fires two events. * `BR-VFC-009` — the historical-import bypass, seen from the VeriFactu side. * `BR-VFC-015` — the QR block and its two legends. * `BR-VFC-017` — the sandbox/production boundary and the prohibition on silent simulation. * `BR-VFC-018` — `no_verifactu` mode: local chain, event summaries, no real-time transmission. * `BR-VFC-023` — the asymmetric mode switch and the year-end lock, including the guard that stops the activation flag from bypassing it. * `BR-VFC-025` — per-company activation, default off, effective value as `instance AND company`. * `BR-INV-011` — the historical-import bypass, seen from the invoicing side. --- # VeriFactu submission states (/guides/verifactu-submission-states) Every invoice your company issues under VeriFactu produces a **billing record**: a signed XML declaration that is transmitted to the AEAT and cryptographically chained to the previous record of the same company. The invoice and its record are two different objects with two different lifecycles — an invoice can be `sent` and paid while its record is still `rejected` by the AEAT. This page is about the **record**. If you integrate against Factuarea and you only watch invoice status, you will not notice that the tax authority refused a declaration. ## When this applies [#when] The record lifecycle applies to every company that has VeriFactu effectively enabled, from the moment an invoice leaves `draft`. It does **not** apply to: * Companies still in `no_verifactu` mode: records are still created and chained locally, but never transmitted, so they stay outside the accepted/rejected cycle ([`BR-VFC-018`](#traceability), RD 1007/2023 art. 16). * Historical invoices imported with the VeriFactu step skipped — no record is created at all, so there is nothing to poll ([`BR-VFC-009`](#traceability)). Read [VeriFactu auto-submission](/guides/verifactu-auto-submission) for the gates that decide whether a record is created in the first place. ## The five states [#states] | `status` | Meaning | Terminal? | What you do | | ----------- | ------------------------------------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------- | | `pending` | The record exists and is chained, but has not been transmitted yet. | No | Nothing. Transmission is queued. | | `submitted` | Sent to the AEAT, awaiting the definitive answer. | No | Nothing. Poll. | | `accepted` | The AEAT registered the declaration. `aeat_csv` is populated. | **Yes — immutable** | Nothing. To correct the invoice, issue a [corrective](/guides/corrective-invoices). | | `rejected` | The AEAT refused it because of a **data** error (unknown recipient tax ID, schema, totals). | Definitive answer, but repairable | Fix the data, then `subsanar`. | | `error` | **Technical** transmission failure: timeout, AEAT unreachable, signature problem. | No | Nothing, or force a `retry`. | The distinction that matters is `rejected` versus `error`. `rejected` is the AEAT saying "I read your declaration and it is wrong". `error` is the declaration never arriving. They are repaired by different operations, and confusing them is the most common integration mistake on this endpoint. `accepted` is the only genuinely immutable state: the transition matrix refuses every transition out of it, because RD 1007/2023 makes a registered record unalterable. Every other state can transition again, which is what makes retry and subsanación possible. ## What the API sends [#api] You never create a record with a payload — it is created for you. What you do is read it. The v1 record object is returned by [`GET /v1/verifactu/records/{id}`](/api-reference/verifactu/public-api.v1.verifactu.records.show), [`GET /v1/verifactu/records`](/api-reference/verifactu/public-api.v1.verifactu.records.list) and, keyed by invoice, by [`GET /v1/invoices/{id}/verifactu`](/api-reference/verifactu/public-api.v1.invoices.verifactu_get): | Field | Meaning | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | One of the five states above. | | `type` | `ALTA` (the invoice was issued) or `ANULACION` (it was annulled). | | `invoice_type` | The AEAT invoice type frozen at issue time: `F1`, `F2`, `F3`, `R1`–`R5`. | | `huella` | The SHA-256 fingerprint of **this** record, uppercase hex. It is the chain link the next record will point at. | | `aeat_csv` | The *Código Seguro de Verificación* the AEAT returns on acceptance. `null` until then. This is the value you reconcile against the tax authority. | | `aeat_submission_id` | Our transmission identifier, for support conversations. | | `transmitted_at` | ISO 8601 of the last transmission that reached the AEAT. Populated in `submitted` and `accepted`; `null` in `pending`, `rejected` and `error`. | | `environment` | The AEAT environment **for this company** — production or the AEAT test bed. A CSV obtained in the test bed is not a real registration. | | `is_simplificada` / `is_substitute_for_simplified` | Whether the source invoice was an `F2`, and whether this record substitutes simplified invoices with an `F3`. | Two fields deserve their own warning. **`huella` is identity, not a checksum you may recompute.** It is calculated from the issuer tax ID, series and number, issue date, invoice type, total tax amount, total amount, the *previous* record's fingerprint and the generation timestamp — in that exact order and format. If any of those change, the chain breaks and the whole company's integrity proof fails ([`BR-VFC-013`](#traceability)). This is why some corrections cannot be repaired in place; see [Retry or subsanar](#retry-vs-subsanar). **`aeat_csv` is written once.** On acceptance it is persisted and never overwritten, even if the AEAT returns the same CSV on a later transmission. A record that goes `rejected` after having been `submitted` keeps the previous CSV for audit purposes, so a non-null `aeat_csv` on a `rejected` record is expected, not a bug ([`BR-VFC-016`](#traceability)). ```bash curl https://api.factuarea.com/v1/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` ```json { "data": { "id": "0197b3c9-1de2-7c40-8b71-a4d5e6f70123", "object": "verifactu_record", "type": "ALTA", "invoice_type": "F1", "invoice_number": "F-2026-0042", "date": "2026-05-15", "amount": 121.0, "status": "accepted", "huella": "9F2C1A0B7E4D6835A1C0B9E8D7F6A5B43C2D1E0F9A8B7C6D5E4F3A2B1C0D9E8F", "aeat_submission_id": "sub_0197b3c9", "aeat_csv": "FCT-2026-A1B2C3D4-E5F6", "environment": "production", "transmitted_at": "2026-05-15T09:41:02Z", "is_simplificada": false, "is_substitute_for_simplified": false, "created_at": "2026-05-15T09:40:58Z" } } ``` ### The retry budget is real, and it is not in the payload [#retry-budget] Behind `error` there is a counter and a schedule. A failed transmission is re-queued with exponential backoff, and the number of blind technical retries is capped per transmission round; once the cap is exhausted, a further manual retry answers with a business-rule error instead of re-queueing ([`BR-VFC-006`](#traceability)). Alongside the counter the record carries a technical-incident marker, raised when the AEAT itself was unreachable and the incident had to be declared — it is preserved even after a later acceptance, for audit. **None of those three values — the attempt counter, the next scheduled retry and the incident marker — are exposed in the v1 record object.** They govern the behaviour you observe, but you cannot read them over the public API today. What you *can* observe is the state itself, `transmitted_at`, and the record's audit timeline via [`GET /v1/verifactu/records/{id}/activities`](/api-reference/verifactu/public-api.v1.verifactu.records.activities). Do not build a client-side model of the retry schedule from guesses: poll the state. ## Retry or subsanar [#retry-vs-subsanar] Both operations act on a record that already exists. They are not interchangeable. | Record state | Cause | Operation | Why | | --------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `error` | The declaration never reached the AEAT. | [`POST /v1/verifactu/records/{id}/retry`](/api-reference/verifactu/public-api.v1.verifactu.records.retry) | The stored XML is correct. Re-send it unchanged. | | `rejected` | The AEAT read it and refused the data. | [`POST /v1/verifactu/records/{id}/subsanar`](/api-reference/verifactu/public-api.v1.verifactu.records.subsanar) | The XML must be regenerated from corrected master data. | | `accepted` | — | Neither. | The record is immutable. Issue a [corrective invoice](/guides/corrective-invoices). | | `rejected`, but the fix touches a fingerprint field | The wrong total, date, number, tax ID or invoice type was declared. | Neither — annul and re-issue. | Changing a fingerprint field would invalidate the chain. `subsanar` refuses it up front. | Retrying blind consumes the technical retry budget. Subsanación does not: it is a deliberate manual correction with new data, the regulation sets no cap on it, and executing it **resets the transmission round** — the attempt counter goes back to zero and the automatic retransmission of the corrected content gets its full budget again ([`BR-VFC-006`](#traceability), [`BR-VFC-020`](#traceability)). Subsanación regenerates the payload but **embeds the original fingerprint, the original chain link and the original generation timestamp**, because those are what let the AEAT match the resubmission to the record it rejected. Before persisting anything it compares the regenerated fingerprint-relevant fields against the stored ones; if any differs, it answers `422` with the subcode that tells you an annulment is required, and nothing is modified. The full flow, the error subcodes and the prevention advice live in [VeriFactu record subsanación](/guides/verifactu-subsanacion). ## What appears on the PDF [#pdf] The record state does **not** change the PDF. Whatever the state, the invoice prints the same legal QR block in the top-right corner of the first page: the `QR tributario:` label, a 30×30 mm code addressing the AEAT verification service with the issuer tax ID, series and number, date and total, and the legend underneath ([`BR-VFC-015`](#traceability)). Three consequences worth designing for: * The QR is printed as soon as a record exists — including while it is `pending`, `error` or `rejected`. A recipient who scans it before acceptance sees the AEAT reporting no registration. That is correct behaviour, not a defect. * The CSV is **not** printed on the PDF. It is only available through the API and the dashboard. * The fingerprint and the registration timestamp stopped being printed as well. If you were scraping them from the PDF, read them from the record instead. Subsanación is the one operation that also touches the printed document: it deliberately re-freezes the invoice's immutable recipient and issuer snapshots from current master data, so that the PDF matches what was re-declared to the AEAT ([`BR-INV-024`](#traceability), [`BR-VFC-020`](#traceability)). It is the only path that rewrites an already-frozen snapshot. ## What reaches the AEAT [#aeat] Each record transmits one declaration, chained to the previous record of the same company by its fingerprint. Three record kinds exist, and they do not share one chain: registrations (`ALTA`) and annulments (`ANULACION`) share the invoicing chain, while system-event records keep a separate chain of their own, because the regulation treats operational events as separate evidence ([`BR-VFC-014`](#traceability)). When a resubmission follows a rejection, the regenerated declaration additionally carries the AEAT flags stating that the previous submission was refused and the record was therefore never registered. Neither flag enters the fingerprint calculation, so declaring them does not disturb the chain ([`BR-VFC-026`](#traceability)). You can verify the whole chain yourself with [`GET /v1/verifactu/chain/validate`](/api-reference/verifactu/public-api.v1.verifactu.chain.validate), which recomputes every fingerprint and reports anomalies. It is rate-limited to one call per minute per company because it walks the entire ledger. ## Traceability [#traceability] Derived from the domain rules of the Factuarea backend: * `BR-VFC-006` — retry policy: exponential backoff, capped attempts per round, and the cap explicitly not applying to subsanación. * `BR-VFC-013` — the fingerprint chain is immutable and verifiable; any alteration invalidates the AEAT integrity guarantee. * `BR-VFC-014` — the three record kinds and their independent chains. * `BR-VFC-015` — the mandatory QR block on the printed invoice. * `BR-VFC-016` — the CSV as public identity of the record, persisted unaltered. * `BR-VFC-018` — `no_verifactu` mode: local chain, no transmission. * `BR-VFC-020` — subsanación of rejected records, fingerprint guard, and the transmission-round reset. * `BR-VFC-026` — the AEAT flags for a resubmission after rejection. * `BR-INV-024` — the immutable recipient snapshot and the single exception that refreshes it. Also derived from the transmission state machine documented alongside those rules (`AeatTransmissionStatus`), which is the source of truth for the transition matrix quoted in [The five states](#states). --- # VeriFactu record subsanación (/guides/verifactu-subsanacion) When the AEAT rejects a VeriFactu billing record because of a **data error** (record `status: rejected`), the VeriFactu regulation (Royal Decree 1007/2023, art. 11) lets you **subsanar** the record: resubmit the **same record** with the corrected content. It is not a new invoice, not a corrective and not an annulment — the rejected record itself is repaired and transmitted again. ``` POST /v1/verifactu/records/{record}/subsanar ``` * **Scope:** `verifactu:write` * **Request body:** none — the subsanable content is regenerated server-side from the source invoice and the **current** master data. * **Response:** `202 Accepted` — the resubmission is queued and sent to the AEAT within seconds. <Callout type="info"> Subsanación **never recalculates** the original fingerprint (*huella*), the previous-record chain link or the original generation timestamp: the AEAT matches the resubmission to the rejected record precisely because they are preserved. There is **no cap on subsanación attempts** — the technical retry cap applies only to blind retries of transmission failures. </Callout> ## When to use it — and when not [#when] | Situation | What to do | | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Record **`rejected`** by the AEAT for a data error — recipient NIF not identified in the census, wrong recipient name, description issues… | **Subsanación.** Fix the data at its source, then call `POST …/subsanar`. | | Record in **`error`** (technical transmission failure: timeout, AEAT down). | Nothing — transmission retries automatically ([manual retry](/api-reference/verifactu/public-api.v1.verifactu.records.retry) available). Subsanación answers `record_not_rejected`. | | Record **`accepted`** but the invoice carries wrong data. | A **corrective invoice** (R1–R5). An accepted record is immutable — subsanación answers `record_not_rejected`. | | The correction changes a **fingerprint field**: issuer NIF, series + number, issue date, invoice type, total tax amount or total amount. | **Annulment + new record** (new invoice or corrective). Subsanación answers `requires_annulment` without modifying anything. | ## The flow [#flow] <Steps> <Step> **Detect the rejection.** Subscribe to the `invoice.verifactu_failed` [webhook event](/guides/webhooks), or poll the [record list](/api-reference/verifactu/public-api.v1.verifactu.records.list) for `status: rejected`. The record carries the AEAT rejection detail. </Step> <Step> **Fix the data at its source.** The resubmitted content is regenerated from the source invoice and the current master data — e.g. correct the client's NIF or registered name and the new values are picked up automatically. The invoice's frozen legal snapshot is deliberately refreshed so the PDF matches what the AEAT receives. </Step> <Step> **Call the endpoint.** The record re-enters the transmission queue with a fresh round of attempts: ```bash curl -X POST https://api.factuarea.com/v1/verifactu/records/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/subsanar \ -H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW" ``` Response (`202`): ```json { "data": { "id": "0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42", "message": "Subsanación encolada. El registro se reenviará a la AEAT en unos segundos." } } ``` </Step> <Step> **Watch the outcome.** The record is transmitted again and ends `accepted` — or `rejected` once more if the data is still wrong, in which case you can subsanar again (there is no attempt limit). </Step> </Steps> ## Errors [#errors] Business-rule violations return `422` with `code: business_rule_violation` and a `subcode` that pinpoints the cause: ```json { "error": { "type": "invalid_request_error", "code": "business_rule_violation", "subcode": "record_not_rejected", "message": "El registro #842 no está rechazado por la AEAT (estado actual: accepted). Solo los registros rechazados admiten subsanación; para fallos técnicos usa el reintento." } } ``` | HTTP | `code` / `subcode` | When | | ---- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | 404 | `resource_not_found` | The record doesn't exist or belongs to another company. | | 422 | `business_rule_violation` / `record_not_rejected` | The record is not in `rejected` (it is accepted, pending, submitted — or a technical `error`, which the automatic retry covers). | | 422 | `business_rule_violation` / `requires_annulment` | The correction touches fingerprint fields. Annul the record and issue a new invoice or a corrective. | | 422 | `business_rule_violation` / `record_not_subsanable` | The record is not an *alta* record, or it has no source invoice to regenerate from. | | 403 | `insufficient_scope` | The key lacks the `verifactu:write` scope. | ## Avoid the rejection in the first place [#prevention] The most frequent data rejection is a recipient that the AEAT census does not identify (rejection **1239**). Verify the **name + NIF pair** of a client with [`POST /v1/clients/census-verification`](/guides/census-verification#clients) **before** issuing VeriFactu invoices to them — subsanación then remains what it should be: a safety net, not a routine. --- # Versioning (/guides/versioning) The Factuarea API follows a **flat-versioned** URL policy (`/v1`) combined with an optional date header for non-breaking evolution. The commitment is clear: once published, `/v1` stays stable. Incompatible changes require `/v2`. ## URL version [#url-version] ``` https://api.factuarea.com/v1/... ``` `v1` is our first public version (May 2026). No prior versions are accessible. When `/v2` is designed: * `/v1` and `/v2` coexist for **at least 12 months**. * `/v1` routes do not change in that window (no payloads, status codes, fields or semantics). * Email notices to developers with active keys, banner in the docs, headers on responses (see below). ## `Factuarea-Version` header [#factuarea-version-header] ```http Factuarea-Version: 2026-06-01 ``` Date-versioning works the Stripe way. There is a **registry of supported versions** (`YYYY-MM-DD` dates); today there is a single one, `2026-06-01`, which is also the latest. The version that applies to a request — the **effective version** — is resolved in this order: 1. The `Factuarea-Version` **request header**, if you send one. 2. Otherwise, the version **pinned on your API key** (set when the key is created; null means "always latest"). 3. Otherwise, the **latest** version in the registry. The effective version is **echoed on every response** in the `Factuarea-Version` header, so you always know which calendar version served your request. ```http Factuarea-Version: 2026-06-01 ``` Pinning a version (by header or on the key) freezes the behavior of the subset of endpoints that receive non-breaking incremental improvements (new response fields, new optional parameters). Without a header or a pin, you get the latest version. The registry currently holds two dates: **`2026-06-01`** (the default, and what you get without a header or a pin) and **`2026-09-01`**. Opting into the newer date changes two things, and nothing else: | Change in `2026-09-01` | What you get on `2026-06-01` | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Bulk-delete responses use the transversal partial-success shape `{total, successful, failed, failures[{id, error_code, error_message}]}`. | The previous shape `{object: "bulk_delete_result", deleted, failed[{id, reason}]}`. | | The five pre-existing payment-gate errors (`payment_method_required`, `seat_charge_failed`, `gestoria_plan_required`, `employee_seat_payment_method_required`, `employee_seat_charge_failed`) carry `error.type: "payment_required_error"`. | `error.type: "invalid_request_error"` for those five, exactly as before. | <Callout type="info"> The error reclassification does **not** touch `error.code`, `error.subcode` or the HTTP status — they are `402` with the same code on every version. If you branch on `code` (which is what we recommend), nothing changes for you either way. `addon_required` is a newer code and always carries `payment_required_error`. </Callout> ### Errors [#errors] The header is validated against the registry: * **Malformed** value (not `YYYY-MM-DD`, e.g. `2026-05` or `15/05/2026`) → `400 parameter_invalid_format` with `param: "Factuarea-Version"`. * **Well-formed but unsupported** (a valid date that is not in the registry) → `400 unsupported_api_version` with `param: "Factuarea-Version"`. Omitting the header is never an error — it falls back to the key pin or the latest version. ## What is breaking? [#what-is-breaking] We consider **breaking** (forbidden in `/v1`): * Renaming / removing JSON response fields. * Changing a field's type (`string` → `int`). * Changing the `type`/`code` of an existing error envelope. * Changing status codes (e.g. returning `201` where it used to be `200`). * Making a previously optional request field required. * Changing the format of an identifier (UUID v7 stays UUID v7). * Removing an endpoint without a documented replacement and migration window. * Changing the document state machine semantics. We consider **non-breaking** (allowed without a new version): * Adding new fields in responses. * Adding optional parameters in requests. * Adding new endpoints. * Relaxing restrictions (raising a limit, accepting more formats). * Adding new enum values **to fields that aren't critical to client-side state machines**. * Improving error messages (changes `message`, not `type`/`code`). * Reclassifying the `type` of an existing error envelope **behind a dated version**: keys pinned to an earlier date keep receiving the previous `type` byte for byte, and `code`/`subcode`/status never move. This is how the five payment-gate errors were recategorised in `2026-09-01`. Doing it *without* a dated version is the breaking case listed above. ## Deprecation policy [#deprecation-policy] When an endpoint or field is marked deprecated within `/v1` (e.g. a legacy alias replaced by a canonical version): * Email notice to developers with active keys affected. * Banner on `docs.factuarea.com` with the changelog. * Headers on every response of the deprecated endpoint for **at least 12 months** before retirement (which only happens in `/v2`): ```http Deprecation: true Sunset: Wed, 15 May 2027 00:00:00 GMT Link: <https://docs.factuarea.com/changelog#v1-deprecations>; rel="deprecation" Link: <https://docs.factuarea.com/guides/migration-from-holded>; rel="alternate" ``` * In `/v1` the endpoint **keeps working** until the launch of `/v2`. The headers warn. * In `/v2` the endpoint is retired / replaced. The window between the first warning and `/v2` is ≥ 12 months. ## Migration between versions [#migration-between-versions] Each migration (`v1 → v2`) ships with: * A dedicated guide at `docs.factuarea.com/guides/migration-v1-v2`. * Field-to-field and endpoint-to-endpoint mapping. * Operational recommendations (keep both keys, dual-write during the transition). * Webhooks: old events keep their shape; new events live in their own version declared in the payload. ## Changelog [#changelog] Every `/v1` change (new field, deprecation, new event, validation fix) is published in [Changelog](/changelog/launch) with tags: * `feature` — new field / endpoint / event. * `fix` — bug fix. * `deprecation` — field or endpoint marked obsolete (still active in `/v1`). * `breaking` — only appears in `/v2`, never inside `/v1`. * `security` — fix with security implications. Read it first. Subscribe to the RSS feed at `https://docs.factuarea.com/changelog.rss` or follow `@factuarea` on X for announcements. ## Stability commitment [#stability-commitment] <Callout type="info"> An integration built today against `/v1` will keep working in `/v1` for **at least 24 months** from today, without touching your code. Support window for v1 → minimum 12 months after launch of v2. </Callout> That's the guarantee. Any exception will be communicated with generous timelines. --- # Webhooks (/guides/webhooks) Webhooks notify your server when an event happens in Factuarea (invoice paid, quote accepted, client created, etc.) without you having to poll. Each event is delivered to your URL through a signed HTTPS `POST`. <Callout type="warn"> **Not delivered in test mode.** Events generated with a `fact_test_` (sandbox) key are recorded but **never delivered** to your external endpoints. To exercise your receiver's signature verification in sandbox, use the dedicated `POST /v1/webhook_endpoints/{id}/ping`, which *is* delivered. See [Test mode & sandbox](/guides/test-mode). To validate your real handler end-to-end, use [`test_event`](#test-deliveries) instead. </Callout> ## Create an endpoint [#create-an-endpoint] ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.mycompany.com/factuarea/webhook", "description": "Sync with internal CRM", "enabled_events": [ "invoice.created", "invoice.paid", "quote.approved" ] }' ``` Response (the `secret` is returned **only once**): ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b", "object": "webhook_endpoint", "url": "https://app.mycompany.com/factuarea/webhook", "description": "Sync with internal CRM", "enabled_events": ["invoice.created", "invoice.paid", "quote.approved"], "status": "enabled", "secret": "whsec_01HKQS5N8VR7QXJ9K3T6BWPMZA9876543210ABCDEF", "created_at": "2026-05-15T10:23:18Z" } ``` To subscribe to **all events**, pass `"enabled_events": ["*"]`. The full catalog is at [Events](/guides/events). ## HMAC SHA256 signature [#hmac-sha256-signature] Each delivery includes these headers: ```http Factuarea-Signature: t=1747314060,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd Factuarea-Event-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d Factuarea-Event-Type: invoice.paid Factuarea-Delivery-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c Idempotency-Key: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d ``` * `t` — UNIX timestamp of the delivery (seconds). * `v1` — HMAC SHA256 of the string `{t}.{body}` with the endpoint `secret`, in hex. * `Idempotency-Key` — the standard industry header, with the **same value** as `Factuarea-Event-Id` (the stable UUID v7 of the event). Use it to deduplicate redeliveries with the header your stack may already understand. See [Idempotency on your side](#idempotency-on-your-side). <Callout type="info"> **Using an official SDK?** Skip the manual HMAC below — both the [TypeScript and PHP SDKs](/sdks) ship a webhook verifier that does the constant-time comparison, the timestamp tolerance and the rotation-grace handling for you. See [Verifying webhooks with the SDK](/sdks#verifying-webhooks). The manual recipe below is for any other language. </Callout> To validate: 1. Extract `t` and `v1` from `Factuarea-Signature`. 2. Compute `signed_payload = t + "." + raw_body` (raw bytes of the body, without reformatting the JSON). 3. Compute `expected = hmac_sha256(secret, signed_payload)` in hex. 4. Compare `v1 == expected` using **constant-time comparison**. 5. Check that `|now - t| <= 300` (±5-minute tolerance against replay attacks). <Callout type="info"> During a secret-rotation grace window the header carries **two** `v1` values — one per active secret (`t=...,v1=<current>,v1=<previous>`). Accept the request if **any** `v1` matches. See [Secret rotation](#secret-rotation-dual-signing). </Callout> <Tabs items="['PHP', 'Node.js', 'Python (Flask)']"> <Tab value="PHP"> ```php function verifyFactuareaSignature( string $payload, string $signatureHeader, string $secret, int $toleranceSeconds = 300, ): bool { $timestamp = null; $signatures = []; foreach (explode(',', $signatureHeader) as $kv) { [$k, $v] = explode('=', $kv, 2); if ($k === 't') { $timestamp = (int) $v; } elseif ($k === 'v1') { $signatures[] = $v; } } if ($timestamp === null || $signatures === []) { return false; } if (abs(time() - $timestamp) > $toleranceSeconds) { return false; } $expected = hash_hmac('sha256', $timestamp.'.'.$payload, $secret); foreach ($signatures as $candidate) { if (hash_equals($expected, $candidate)) { return true; } } return false; } // In the webhook handler: $payload = file_get_contents('php://input'); $header = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; $secret = getenv('FACTUAREA_WEBHOOK_SECRET'); if (! verifyFactuareaSignature($payload, $header, $secret)) { http_response_code(401); exit; } $event = json_decode($payload, true); handleEvent($event); http_response_code(200); ``` </Tab> <Tab value="Node.js"> ```javascript const crypto = require('crypto'); function verifySignature(payload, header, secret, tolerance = 300) { let timestamp = null; const signatures = []; for (const kv of header.split(',')) { const [k, v] = kv.split('='); if (k === 't') timestamp = Number(v); else if (k === 'v1') signatures.push(v); } if (timestamp === null || signatures.length === 0) return false; if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${payload}`) .digest('hex'); return signatures.some((candidate) => crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(candidate, 'hex') ) ); } // Express: app.post('/factuarea/webhook', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString('utf8'); if (!verifySignature(payload, req.header('Factuarea-Signature'), process.env.WHSEC)) { return res.status(401).end(); } const event = JSON.parse(payload); handleEvent(event); res.status(200).end(); }); ``` </Tab> <Tab value="Python (Flask)"> ```python import hmac, hashlib, time from flask import request, abort def verify(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool: timestamp = None signatures = [] for kv in header.split(','): k, v = kv.split('=', 1) if k == 't': timestamp = int(v) elif k == 'v1': signatures.append(v) if timestamp is None or not signatures: return False if abs(time.time() - timestamp) > tolerance: return False signed = f"{timestamp}.".encode() + payload expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, candidate) for candidate in signatures) @app.post('/factuarea/webhook') def webhook(): if not verify(request.get_data(), request.headers.get('Factuarea-Signature', ''), WHSEC): abort(401) event = request.get_json() handle_event(event) return '', 200 ``` </Tab> </Tabs> ## Verify with the official SDK [#verify-with-the-official-sdk] The [TypeScript and PHP SDKs](/sdks) wrap the five steps above — constant-time comparison, the ±5-minute tolerance and the rotation-grace window — behind one call. Pass the **raw request body**, the `Factuarea-Signature` header and the endpoint secret: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Express, with express.raw({ type: "application/json" }) on the route: app.post("/webhooks/factuarea", (req, res) => { try { const event = factuarea.webhooks.verify( req.body.toString("utf8"), req.headers[SIGNATURE_HEADER.toLowerCase()] as string, process.env.FACTUAREA_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { /* … */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` A custom tolerance (in seconds) is the optional fourth argument: `factuarea.webhooks.verify(body, header, secret, { toleranceSeconds: 600 })`. </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier; use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException; $verifier = new WebhookVerifier(); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; try { $event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET')); // $event is the decoded, authenticated payload if (($event['type'] ?? null) === 'invoice.paid') { /* … */ } http_response_code(200); } catch (WebhookSignatureException $e) { http_response_code(400); } ``` </Tab> </Tabs> Both verifiers accept **both** `v1` signatures during a secret-rotation grace window (see [Secret rotation](#secret-rotation-dual-signing)), so a rotation never drops a delivery. ## Retries [#retries] If your endpoint responds with a status that's **not `2xx`** or doesn't respond within the endpoint `timeout_seconds` (default 10 s), Factuarea retries with exponential back-off: | Attempt | Delay after previous | | ------- | -------------------- | | 1 | immediate | | 2 | 1 minute | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 2 hours | | 6 | 12 hours | | 7 | 1 day | | 8 | 3 days | After the final attempt the delivery moves to `failed_permanently` and stops retrying. It remains visible in `GET /v1/webhook_endpoints/{id}/deliveries` for 30 days, and you can retry it manually via `POST /v1/webhook_endpoints/{id}/deliveries/{delivery_id}/replay` or from the dashboard. ## Delivery body [#delivery-body] The delivery body is the [event object](/guides/events) itself: ```json { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0d", "object": "event", "type": "invoice.paid", "api_version": "2026-05-22", "livemode": true, "test": false, "data": { "invoice": { "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a03" } } } ``` The `data` field holds a **thin reference** to the affected resource — fetch it from its own endpoint for the full representation. The `api_version` is always present on delivered events (`null` only for legacy events emitted before versions were sealed). The `test` field is always present: `true` only for a **test delivery** triggered from the dashboard (see below), `false` for real events. It is **orthogonal to `livemode`**: `test` says whether *this delivery* is a test, while `livemode` reflects the **key environment** (live vs sandbox). A test delivery can be issued in either, so `livemode: true, test: true` is valid. ## Expected response [#expected-response] * Status `200`, `201`, `202` or `204` → delivery `delivered`. * Any other status → delivery `failed`, next retry scheduled. * Body irrelevant. We **don't** process it — only `response_status` and `duration_ms` are stored in the delivery log. ## Replay from the dashboard [#replay-from-the-dashboard] `Developers > Webhooks > Deliveries` lets you manually retry any delivery, even `failed_permanently` ones. A manual retry resets the counter and leaves an audit log entry. ## Test deliveries [#test-deliveries] Two endpoints let you exercise your receiver without waiting for a real event — they solve different problems: * `POST /v1/webhook_endpoints/{id}/ping` sends a **synthetic** `webhook.ping` payload. It never enters the event log and isn't a real event type. Use it to confirm reachability and signature verification (it's the only delivery that fires in **sandbox**). * `POST /v1/webhook_endpoints/{id}/test_event` triggers a **test delivery of a real catalog event type**, marked `"test": true` in the envelope. It records a real `Event` (visible in `GET /v1/events`) and queues a `WebhookDelivery` signed and retried **exactly like a production delivery** — so you validate your real handler end-to-end. ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints/{id}/test_event \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "invoice.paid" }' ``` `type` is optional: omit it to use the endpoint's first subscribed event. If given, it must be one of the endpoint's `enabled_events` (otherwise `422 event_not_subscribed`). The delivery reaches **only this endpoint**, never the other endpoints subscribed to the same type. ## Secret rotation (dual-signing) [#secret-rotation-dual-signing] ```bash curl -X POST https://api.factuarea.com/v1/webhook_endpoints/{id}/rotate_secret \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Returns the new secret. For **24 hours** (the `previous_secret_valid_until` instant in the response) both secrets are valid: each delivery is signed **twice** in the same `Factuarea-Signature` header — one `v1` per secret (`t=...,v1=<current>,v1=<previous>`). After the window, the old secret is invalidated. Lets you roll out the new secret with zero downtime: 1. Call `/rotate_secret` → get the new `secret`. 2. Deploy the new secret to your env. 3. Your handler accepts either `v1` during the grace window (the verification helpers above already loop over every `v1`). 4. After the window, only the new secret is in use. ## Idempotency on your side [#idempotency-on-your-side] Every event includes an `id` field (UUID v7, unique). Retries of the same event always carry the same `id` — and the `Idempotency-Key` header carries that exact value too, so you can deduplicate from the header without parsing the body. Persist the ids you've processed (table `webhook_events_processed`) and return `200` without acting if you've already processed it. ```python event_id = event['id'] if db.exists('webhook_events_processed', id=event_id): return '', 200 process(event) db.insert('webhook_events_processed', id=event_id, processed_at=now()) return '', 200 ``` ## IP allowlist (optional) [#ip-allowlist-optional] If your endpoint runs behind a firewall that filters by IP, you can restrict source IPs via `ip_allowlist` when creating the endpoint. Factuarea delivers from a pool of stable IPs documented in the dashboard. <Callout type="warn"> **Validate the HMAC signature, not the IP** — IPs can change with 30 days' notice, signatures cannot. </Callout> ## Available events [#available-events] The full catalog is returned by `GET /v1/event-catalog` and documented at [Events](/guides/events). Key examples: * `invoice.created`, `invoice.updated`, `invoice.sent`, `invoice.paid`, `invoice.annulled` * `quote.created`, `quote.approved`, `quote.rejected`, `quote.converted` * `proforma.accepted`, `proforma.converted_to_invoice` * `delivery_note.signed` * `facturae.face_submitted`, `facturae.face_status_changed`, `facturae.face_cancellation_requested` * `client.created`, `client.updated` --- # Work schedules (/guides/work-schedules) A **work schedule** models the hours a company **expects** from an employee: how many hours per day and at what time the day starts. It feeds two downstream calculations — the **expected hours** used for balances, and the **planned start time** used to flag late arrivals in [presence](/guides/presence). Schedules are scoped by `work_schedules:read` / `work_schedules:write` under `https://api.factuarea.com/v1`. ## The weekly schedule [#schedule] A **weekly schedule** carries a name, a **week pattern** of seven days — each day a list of non-overlapping `HH:MM–HH:MM` ranges — a **mode**, and a status (`active` / `archived`). The expected weekly hours and the planned start are **derived** from the pattern. The **mode** sets how compliance is measured: | Mode | Meaning | | --------------- | -------------------------------------------------------------------------------------------- | | `validated` | The expected hours are taken as worked once validated — the schedule is the source of truth. | | `real_clocking` | Compliance is measured against the actual clock entries in the ledger. | The default mode is `validated`. | Operation | Endpoint | | ------------------- | ---------------------------------------------------------------- | | List / show | `GET /v1/work-schedules`, `GET /v1/work-schedules/{schedule}` | | Create / update | `POST /v1/work-schedules`, `PATCH /v1/work-schedules/{schedule}` | | Archive / unarchive | `POST /v1/work-schedules/{schedule}/archive`, `.../unarchive` | | Stats | `GET /v1/work-schedules/stats` | ```bash curl -X POST https://api.factuarea.com/v1/work-schedules \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Full-time 9 to 5", "mode": "validated", "week_pattern": { "monday": [{ "start": "09:00", "end": "17:00" }], "tuesday": [{ "start": "09:00", "end": "17:00" }], "wednesday": [{ "start": "09:00", "end": "17:00" }], "thursday": [{ "start": "09:00", "end": "17:00" }], "friday": [{ "start": "09:00", "end": "17:00" }], "saturday": [], "sunday": [] } }' ``` A day with an empty list is a rest day. See the schemas in the [API Reference](/api-reference/work-schedules/public-api.v1.work_schedules.create). ## Assignments [#assignments] A schedule applies to an employee through an **effective-dated assignment**: an `effective_from` (inclusive) and an optional `effective_to` (exclusive). Assigning a new schedule to an employee **closes the previous open assignment**, so an employee has one effective schedule at any date without gaps or overlaps. | Operation | Endpoint | Effect | | ------------------------- | ----------------------------------------------- | -------------------------------------------------------------------- | | Assign | `POST /v1/work-schedules/{schedule}/assign` | Opens an assignment from `effective_from`, closing the previous one. | | Unassign | `POST /v1/work-schedules/{schedule}/unassign` | Closes the employee's open assignment to this schedule. | | List assignments | `GET /v1/work-schedules/{schedule}/assignments` | The employees currently assigned. | | Resolve employee schedule | `GET /v1/work-schedules/employee/{employee}` | The schedule effective for an employee on a given date. | ```bash curl -X POST https://api.factuarea.com/v1/work-schedules/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/assign \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "employee_id": "01931b3e-7c4a-7f2e-9a8b-4d6e7f8a9b0c", "effective_from": "2026-01-07" }' ``` `GET /v1/work-schedules/employee/{employee}` is the contract that balances and presence consume: it returns the schedule in force for the employee on the requested date, from which the expected hours and the planned start are read. <Callout type="info"> Assignments are date-ranged, not a single field on the employee. Reassigning a schedule never rewrites history — the previous assignment is closed with an `effective_to`, and the new one opens from its `effective_from`. </Callout> ## Typical flow [#flow] 1. Create a **weekly schedule** with its week pattern and mode. 2. **Assign** it to employees from an `effective_from` date. 3. Downstream, the schedule feeds the **expected hours** for balances and the **planned start** that [presence](/guides/presence) uses to flag late arrivals. 4. **Unassign** or reassign as contracts change; **archive** schedules you no longer use. ## Next steps [#next] * [Presence](/guides/presence) — how the planned start powers late-arrival detection. * [Monthly close](/guides/monthly-time-close) — where expected vs worked hours are reported. --- # Time tracking overview (/guides/workforce-overview) Factuarea's **time tracking** (control horario) covers the legal duty of Spanish employers under **RD-ley 8/2019** (art. 34.9 of the Workers' Statute): keep an **objective, reliable and unalterable** daily record of every employee's working day, retain it for **four years** and make it available to the Labour Inspectorate (ITSS). The record is built on an **append-only ledger** sealed by a **per-company SHA-256 hash chain** — the same tamper-evidence pattern Factuarea uses for [VeriFactu](/guides/glossary) invoicing. It is, in short, the VeriFactu of attendance: nothing is ever edited or deleted, and any manipulation breaks the chain. Every operation lives under `https://api.factuarea.com/v1` and shares the same [error envelope](/guides/errors), [cursor pagination](/guides/pagination) and [scopes](/guides/scopes-and-irreversibility) as the rest of the API. The whole surface is gated by the **`control_horario` module**; a company without it gets a `403` on these routes. ## The employee, a portal-only role [#employee-role] An **employee** is the worker who clocks in, has a schedule, requests absences and accrues day balances. It is a **portal-only** role: employees operate their own data from the portal and are **never** counted against the plan `users` seat limit. Adding employees is instead billed through a dedicated per-seat add-on — see [Employee seat billing](/guides/employee-seats). ## The eight domains [#domains] The system is split into eight API domains. Start with the guide for the task at hand; each links to its endpoints in the API Reference. | Domain | What it does | Guide | Scope | | --------------- | --------------------------------------------------------- | ------------------------------------------- | ---------------------------------------------- | | Employees | The staff roster: create, update, deactivate, reactivate. | — | `employees:read` / `employees:write` | | Work schedules | Expected weekly hours and effective-dated assignments. | [Work schedules](/guides/work-schedules) | `work_schedules:read` / `work_schedules:write` | | Time entries | Clock in/out, pauses, retroactive entries, corrections. | [Time clock](/guides/time-clock) | `time_entries:read` / `time_entries:write` | | Monthly closes | Freeze, seal, report and export the monthly register. | [Monthly close](/guides/monthly-time-close) | `time_entries:read` / `time_entries:write` | | Payroll exports | Incidents file for A3, Sage or NominaSOL. | [Monthly close](/guides/monthly-time-close) | `payroll_exports:read` | | Absences | Types, policies, requests, balances and calendar. | [Absences](/guides/absences) | `absences:read` / `absences:write` | | Presence | Who is working now, in office or remote. | [Presence](/guides/presence) | `presence:read` | | Public holidays | National, regional and local calendar per region. | — | `holidays:read` | Two domains are **read-only** over the API: **presence** and **public holidays** expose only reads (`presence:read`, `holidays:read`). Declaring office/remote presence and creating custom local holidays are portal-only tasks — there is no `presence:write` nor `holidays:write` scope. ## Employees and their roster [#employees] The employee is the anchor entity the rest of the system depends on. Each employee carries a name, a company-unique email, an optional `tax_id` and `job_title`, contracted weekly hours, a hire date and the autonomous community (`ccaa`) that drives which public holidays apply. Deactivation is a **soft** termination: the employee keeps their ledger history (the four-year retention forbids destroying it) and can be reactivated later. ```bash curl -X POST https://api.factuarea.com/v1/employees \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Ana Ruiz", "email": "ana.ruiz@acme.example", "employment_type": "full_time", "contract_hours": 40, "hire_date": "2026-01-07", "ccaa": "ES-MD" }' ``` See the full employee schemas in the [API Reference](/api-reference/employees/public-api.v1.employees.list). ## Scopes and MCP [#scopes] Every domain maps to a fine-grained scope from the closed catalogue (`employees:*`, `time_entries:*`, `work_schedules:*`, `absences:*`, `presence:read`, `holidays:read`, `payroll_exports:read`), all gated behind the `control_horario` module. Review the full list on the [scopes page](/guides/scopes-and-irreversibility) and the [MCP scopes catalogue](/mcp/scopes). Each v1 route mirrors a public [MCP tool](/mcp/tools), so an agent can drive the same operations. <Callout type="info"> The time-record ledger is compliance data, isolated by design: it never references clients, invoices or projects. It answers one question — how many hours each employee worked — and keeps that evidence intact. </Callout> ## Where to go next [#next] * [Time clock](/guides/time-clock) — clock in/out, pauses and the correction workflow. * [Monthly close](/guides/monthly-time-close) — freeze, seal and export the register. * [Absences](/guides/absences) — types, policies, requests, balances and carryover. * [Work schedules](/guides/work-schedules) — weekly patterns and assignments. * [Presence](/guides/presence) — the live team panel and daily office/remote view. * [Employee seat billing](/guides/employee-seats) — the per-seat add-on and its cycle. --- # MCP overview (/mcp) The **Factuarea MCP server** exposes the public API as [Model Context Protocol](https://modelcontextprotocol.io) tools, so AI agents (Claude, ChatGPT, Cursor, your own LLM app) can read and operate on your invoicing data through a single, governed endpoint instead of hand-writing HTTP calls. It speaks the **Streamable HTTP** transport and lives at: ``` https://mcp.factuarea.com ``` <Callout type="info"> The canonical endpoint is the subdomain root. The earlier path form `https://mcp.factuarea.com/mcp` continues to work as a compatibility alias, so existing configurations keep connecting. </Callout> Every tool maps to the same `https://api.factuarea.com/v1` contract documented in the API Reference: identical resources, the same opaque `id` (UUID v7), the same normalized errors, the same multi-tenant isolation by company. The MCP layer adds discovery (`tools/list`), per-tool **scope** enforcement and a consent flow for third-party apps. <Cards> <Card icon="<Boxes />" title="Install the Claude Code plugin" href="/mcp/claude-code-plugin"> The recommended setup — two commands install the official `factuarea-mcp` plugin and connect Claude Code over OAuth. </Card> <Card icon="<Bot />" title="Connect any client" href="/mcp/connect"> Wire up Claude Desktop, the MCP Inspector or any MCP client manually, using OAuth or an API key. </Card> <Card icon="<Wrench />" title="<>Browse the <Stat n="tools" /> tools</>" href="/mcp/tools"> The full catalog grouped by domain, with the scope each tool requires. </Card> </Cards> ## What it can do [#what-it-can-do] The server publishes **<Stat n="tools" /> tools** across 27 domains. Anything you can do with the REST API you can do here, in the agent's native tool-calling format: <Cards> <Card icon="<Boxes />" title="Sales & purchases" href="/mcp/tools#invoice"> Invoices, quotes, pro-forma invoices, delivery notes, recurring invoices and vendor bills — create, update, transition, send and generate PDFs. </Card> <Card icon="<Code />" title="Catalog & CRM" href="/mcp/tools#client"> Clients, suppliers, products, numbering series and tax rates. </Card> <Card icon="<ShieldCheck />" title="Compliance" href="/mcp/tools#verifactu"> VeriFactu (AEAT) records, events and certificates, FACe (FacturaE) submissions, plus webhooks and the event catalog. </Card> <Card icon="<Clock />" title="Time tracking" href="/mcp/tools#employee"> Employees, work schedules, the time-clock ledger, monthly closes, absences, presence and public holidays — the RD-ley 8/2019 register. </Card> </Cards> ## When to use MCP vs REST vs SDKs [#when-to-use-mcp-vs-rest-vs-sdks] The MCP server, the REST API and the official SDKs are three front doors to the **same** backend. Pick by who (or what) is calling: | You are building… | Use | Why | | ---------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | An **AI agent / assistant** that reasons over your data and acts on it | **MCP server** | Tools are self-describing; the model discovers and calls them without you wiring each endpoint. Scopes and consent are enforced per call. | | A **backend service, cron job or integration** with fixed logic | **REST API** | Deterministic, no model in the loop, full control over requests and retries. | | A **typed client** in your app (TypeScript or PHP) | [**Official SDKs**](/sdks) | `@factuarea/sdk` and `factuarea/factuarea-php` wrap the REST API with types, retries and idempotency helpers. | <Callout type="info"> The three surfaces share the same identifiers, error envelope and scopes, so you can mix them: prototype a flow with an agent over MCP, then harden the critical path as a REST or SDK integration. </Callout> ## Two ways to authenticate [#two-ways-to-authenticate] The same `/mcp` endpoint accepts two kinds of credential, for two different audiences: | Channel | Credential | For | Reachable tools | | ------------- | ------------------------------------------ | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **API key** | `fact_live_…` / `fact_test_…` Bearer token | **The account owner** automating their own company (like a GitHub PAT) | Up to **<Stat n="tools" />** — you grant any scopes you want, including `*` | | **OAuth 2.1** | Access token issued via consent | **Third-party apps** acting on a user's behalf | **<Stat n="oauth_reachable" />** — a curated catalog that excludes VeriFactu writes, GDPR erasure, FacturaE (FACe), Payments & gateways, and the gestoría and account-write tools | See [Connecting a client](/mcp/connect#channel-policy) for the full channel policy, and [Scopes & permissions](/mcp/scopes) for the scope catalog. ## Build in test mode first [#build-in-test-mode-first] Just like the REST API, a `fact_test_` key — or an OAuth consent with the **Test** environment selected — operates on an isolated **sandbox company** with external effects switched off (no AEAT transmission, no real emails, no outbound webhooks). Build and validate against test, then switch to live. See [Test mode](/mcp/connect#test-mode). <Callout type="info"> The MCP server is **included in every Factuarea plan**, alongside the rest of the public API. Create an API key from [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) (or connect via OAuth) and start calling tools. </Callout> --- # Claude Code plugin (/mcp/claude-code-plugin) The Factuarea marketplace publishes **two** Claude Code plugins, for two different jobs. **`factuarea-mcp`** is the fastest way to connect [Claude Code](https://claude.com/claude-code) to the Factuarea MCP server: it registers the server (`https://mcp.factuarea.com`) and bundles a skill that teaches Claude how to use the tools well — scopes, cursor pagination, the error envelope and test mode — so you don't have to wire anything by hand. **`factuarea-api`** serves the other audience, the developer who writes the integration code, and deliberately declares **no MCP server** at all. <Callout type="info"> This is the **recommended** way to connect Claude Code. Prefer to wire the server manually (other clients, headless setups)? See [Connecting a client](/mcp/connect). </Callout> ## Install [#install] <Steps> <Step> **Add the marketplace** Register the Factuarea plugin catalog. Run this inside Claude Code: ```text /plugin marketplace add factuarea/claude-plugins ``` </Step> <Step> **Install the plugin you need** ```text /plugin install factuarea-mcp@factuarea ``` Claude Code installs the plugin and registers the `factuarea` MCP server. Writing integration code too? Add [`factuarea-api`](#integrator-skills) as well — the two are complementary. </Step> </Steps> To pull in later updates, run `/plugin marketplace update factuarea`. ## Connect the server [#connect-the-server] The plugin declares the server **without an auth header**, so the recommended path is OAuth — nothing secret is ever pasted into a config file. <Steps> <Step> **Authenticate** ```text /mcp ``` Pick **factuarea**, choose **Authenticate**. Your browser opens the Factuarea consent screen. [Dynamic Client Registration](/mcp/connect#oauth-21) and PKCE happen automatically — there's no client id or secret to paste. </Step> <Step> **Approve** On the consent screen you select the **company**, the **environment** (live or test) and the **scopes** to grant. Sensitive scopes (deletes, `invoices:void`) are flagged and not pre-checked. Claude Code stores the token and refreshes it transparently. </Step> <Step> **Use it** Ask Claude to work with your Factuarea data — "list this quarter's unpaid invoices in test mode", "create a draft invoice for Acme S.L.", "check the VeriFactu chain". The skill loads automatically; you can also invoke it explicitly: ```text /factuarea-mcp:factuarea-mcp ``` </Step> </Steps> ### Connect with an API key instead [#connect-with-an-api-key-instead] For headless setups, or when you already have a `fact_` key, connect with a static header instead of OAuth: ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com \ --header "Authorization: Bearer fact_live_xxxxxxxxxxxxxxxxxxxxxxxx" ``` Use a `fact_test_` key to point at the isolated [sandbox](/mcp/connect#test-mode). The API surface is identical — only the prefix changes the environment. With a key header you do **not** need the OAuth flow; the key authenticates every request. ## What `factuarea-mcp` ships [#what-factuarea-mcp-ships] <Cards> <Card icon="<Wrench />" title="The MCP server" href="/mcp/tools"> The `factuarea` server declaration (`https://mcp.factuarea.com`, HTTP transport), so Claude can call all the Factuarea tools directly. </Card> <Card icon="<BookOpen />" title="A guidance skill" href="/mcp/scopes"> A skill that gives Claude the context to use the tools well — the channel policy, the tool domains and their scopes, UUID v7 identity, cursor pagination, the error envelope and test mode. </Card> <Card icon="<Code />" title="The factuarea-api plugin" href="#integrator-skills"> A separate, lighter install for writing the integration itself — five skills, and no MCP server declaration, so no OAuth and no tools loaded. </Card> </Cards> The guidance skill knows the **channel policy** (an API key reaches all <Stat n="tools" /> tools; OAuth uses the curated <Stat n="oauth_reachable" />, never granting `verifactu:write`, the FacturaE, Payments or gestoría/account-write scopes, or the GDPR signature-forget operation, to third-party apps), how plan/module and feature flags further narrow `tools/list`, and that state changes are **discrete tools** (`mark_invoice_as_paid`, `void_invoice`, `accept_quote`…), not a generic `change_status`. ## Building the integration: the `factuarea-api` plugin [#integrator-skills] The plugin above is for **operating your account** through MCP tools. A second plugin covers the opposite job — **writing the code** that calls the REST API from your own backend: ```text /plugin install factuarea-api@factuarea ``` It declares **no MCP server**, which is what makes it cheap to keep installed: no OAuth consent, and no tool surface loaded into the session. Its five skills load from the task at hand and lean on the official SDKs, the live spec and these docs. | Skill | Loads when the task is… | What it covers | | ------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`factuarea-api`** | Starting out, or asking what the API supports, how auth works, or what the docs say | The entry point: ten golden rules, the two accepted auth headers and the prefix that picks the environment, searching these docs locally with [`factuarea docs`](/cli/usage), and recipes that route to the four skills below | | **`factuarea-implement`** | Wiring the client and making the first calls | Choosing between the [TypeScript](/sdks/typescript) and [PHP](/sdks/php) SDK, resolving the key from the environment, the `data` envelope, [cursor pagination](/guides/pagination), [`Idempotency-Key`](/guides/idempotency) on writes, and starting in the sandbox | | **`factuarea-webhooks`** | Writing or fixing the endpoint that receives deliveries | Raw-body HMAC verification of `Factuarea-Signature`, constant-time comparison, dedup by `Factuarea-Event-Id`, a fast 2xx with the heavy work deferred, the rotation grace window, and local testing with `factuarea listen` | | **`factuarea-audit`** | Reviewing an integration that already exists | Six rule families — signature verification, idempotency on writes, API-key exposure, error handling by `code`, rate limits, document lifecycle — reporting each finding with a severity, a `file:line` and the concrete fix | | **`factuarea-upgrade`** | Realigning after a contract or SDK change | Drift between the code and the live spec, the pinned SDK version against the latest published one, and a report that separates breaking changes from additive ones, in the order to apply them | <Callout type="info"> The two plugins are complementary, not alternatives. `factuarea-mcp` reads and acts on your data through tools; `factuarea-api` never calls the API on your behalf — it writes and reviews the code that does. Teams building an integration usually install both. You can also generate a client yourself from the [OpenAPI spec](/api/openapi). </Callout> ## Troubleshooting [#troubleshooting] | Symptom | Cause | Fix | | ------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **A tool returns `401`** | Not authenticated, or the key/token expired. | Run `/mcp` → **factuarea** → **Authenticate** to (re)start OAuth, or check your API-key header. | | **`insufficient_scope` (`403`)** | The credential lacks the tool's required scope. | Re-authenticate and approve the scope, or use a key that has it. Remember `verifactu:write` and the signature-forget tool are **API-key only**. | | **A tool you expected isn't listed** | `tools/list` is filtered by your scopes and feature flags. | Grant the scope (or use a wider key); confirm the credential's channel can reach it (OAuth excludes the API-key-only tools). This is expected, not a bug. | | **`addon_not_active` (`-32007`)** | The company has no active Factuarea plan that includes API access (e.g. an expired trial). | Subscribe to or renew a plan from the dashboard; the whole MCP surface requires an active plan. | | **`429` with `Retry-After`** | A [rate-limit](/mcp/errors#rate-limits) bucket was hit. | Wait the `Retry-After` seconds before retrying — don't hammer. | See [Errors & rate limits](/mcp/errors) for the full code table. --- # Connecting a client (/mcp/connect) The Factuarea MCP server speaks **Streamable HTTP** at `https://mcp.factuarea.com`. Any MCP-compatible client can connect using one of the two supported credentials: * **OAuth 2.1** — the client registers itself and the user authorizes it through a consent screen. Best for end-user tools. * **API key** — you pass a `fact_live_` / `fact_test_` Bearer token directly. Best for your own automations. <Callout type="info"> The canonical endpoint is the subdomain root, `https://mcp.factuarea.com`. The previous path form `https://mcp.factuarea.com/mcp` keeps working as a compatibility alias. </Callout> <Callout type="info"> **Using Claude Code?** The recommended setup is the official `factuarea-mcp` plugin — two commands and you're connected over OAuth, with a guidance skill bundled in. See the dedicated [Claude Code plugin](/mcp/claude-code-plugin) guide. The manual steps below are for other clients or headless setups. </Callout> ## Claude Code [#claude-code] The smoothest path is the [Claude Code plugin](/mcp/claude-code-plugin) — it registers the server and bundles a guidance skill in one install. If you prefer to wire the server by hand, [Claude Code](https://claude.com/claude-code) also supports remote MCP servers over HTTP with built-in OAuth. ### With OAuth [#with-oauth] <Steps> <Step> **Add the server** ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com ``` </Step> <Step> **Authenticate** Inside Claude Code, run the slash command: ```text /mcp ``` Pick **factuarea**, choose **Authenticate**, and your browser opens the consent screen. Select the **company** to grant access to, the **environment** (live or test) and the **scopes** you want to allow, then confirm. Claude Code stores the resulting token and refreshes it automatically. </Step> <Step> **Use it** Ask Claude to do something — "list my overdue invoices in test mode" — and it discovers and calls the matching tools. </Step> </Steps> ### With an API key [#with-an-api-key] If you'd rather use your own key (no consent flow), pass it as an `Authorization` header: ```bash claude mcp add --transport http factuarea https://mcp.factuarea.com \ --header "Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" ``` The key's scopes determine which tools appear in `tools/list`. A key with `*` sees all <Stat n="tools" /> tools; a narrower key sees only the tools its scopes cover. ## Claude Desktop [#claude-desktop] [Claude Desktop](https://claude.com/download) connects to remote servers through its configuration file. Add an entry under `mcpServers`: ```json { "mcpServers": { "factuarea": { "type": "http", "url": "https://mcp.factuarea.com" } } } ``` On the next launch, Claude Desktop discovers the server and walks you through the OAuth consent flow in your browser. To use an API key instead, add a `headers` object: ```json { "mcpServers": { "factuarea": { "type": "http", "url": "https://mcp.factuarea.com", "headers": { "Authorization": "Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" } } } } ``` <Callout type="info"> The config file lives at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). Restart the app after editing it. </Callout> ## MCP Inspector [#mcp-inspector] The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is the quickest way to explore the catalog and call tools by hand while you build. <Steps> <Step> **Launch it** ```bash npx @modelcontextprotocol/inspector ``` </Step> <Step> **Connect** Set **Transport** to `Streamable HTTP` and **URL** to `https://mcp.factuarea.com`. For OAuth, the Inspector runs the authorization flow for you. For an API key, add an `Authorization: Bearer fact_test_…` header under **Authentication**. </Step> <Step> **Explore** Open **Tools → List Tools** to see every tool your credential can reach, inspect its input schema, and run it with sample arguments. </Step> </Steps> ## Any MCP client [#any-mcp-client] The server follows the MCP spec, so any compliant client works. The essentials: * **Endpoint** — `https://mcp.factuarea.com` (the path form `…/mcp` is a compatibility alias) * **Transport** — Streamable HTTP * **Auth** — `Authorization: Bearer <token>`, where the token is either an API key (`fact_live_` / `fact_test_`) or an OAuth 2.1 access token * **Discovery** — on a `401`, the server returns a `WWW-Authenticate` header pointing at its [Protected Resource Metadata](#discovery) (RFC 9728) so clients can find the authorization server automatically Tool discovery is paginated; the server returns the full catalog (up to 200 tools) in a single `tools/list` response by default, and honors `nextCursor` if your client paginates. ## Authenticate [#authenticate] The MCP server accepts two kinds of credential on the same `https://mcp.factuarea.com` endpoint, for two different audiences. Both arrive as `Authorization: Bearer <token>`; the server tells them apart by the token's shape (`fact_*` → API key, anything else → OAuth access token). ### Channel policy [#channel-policy] This is the single most important rule of the MCP surface: | Channel | Who | How scopes are chosen | Reachable tools | | ------------- | -------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------- | | **API key** | The **account owner** automating their own company | You pick the scopes when you create the key — up to the super-scope `*` | **<Stat n="tools" />** (everything) | | **OAuth 2.1** | A **third-party app** acting on a user's behalf | The user grants scopes on the consent screen, from a **curated catalog** | **<Stat n="oauth_reachable" />** | The model mirrors GitHub: a **personal access token** (API key) is the owner's own credential and may hold any permission, while an **OAuth app** is external and is limited to a vetted set of scopes the user explicitly approves. The <Stat n="oauth_restricted" /> tools an OAuth app can **never** reach (only an API key can) are the most sensitive fiscal and privacy operations, plus the first-party account-management surface: * **VeriFactu writes** (`verifactu:write`) — 8 tools: register/retry/subsanar VeriFactu records and events, upload/activate/revoke FNMT certificates, update VeriFactu settings. These touch AEAT compliance and are owner-only. * **GDPR erasure** (`delivery_notes:gdpr_forget`) — 1 tool: erase signature-audit PII (Art. 17). Privileged, admin-only. * **FacturaE (FACe)** (`facturae:read` / `facturae:write`) — 5 tools: the FACe B2G operations. Their scopes are not in the OAuth consent catalog yet, so they are API-key only for now. * **Payments & gateways** (`stripe_autoinvoicing:*`, `payouts:read`) — 10 tools: Stripe Connect auto-invoicing config, connected accounts and payouts. Fine-grained, API-key-only scopes with no OAuth consent equivalent. * **Managed companies** (`companies:*`, `api_keys:*`) — 16 tools: gestoría management of child companies and their API keys. Managing sub-accounts and credentials is first-party only, never granted by third-party consent. * **Account writes** (`account:write`) — 4 tools: create/rotate/revoke your own API keys and update account personalization. First-party only. Everything else — all <Stat n="oauth_reachable" /> read/write/transition/send tools — is available to both channels. See [Scopes & permissions](/mcp/scopes) for the catalog. ### API keys [#api-keys] An API key is an opaque Bearer token bound to your company, created in the developer dashboard at [Settings → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys). The format and rules are identical to the REST API: ``` fact_live_<24 alphanumeric characters> → production company fact_test_<24 alphanumeric characters> → isolated sandbox company ``` The prefix is the source of truth for the **environment** — see [Test mode](#test-mode). The secret is shown **only once** at creation; the backend stores only a bcrypt hash. Pass it to your MCP client as: ``` Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxx ``` For the full key lifecycle — creation, scopes, rotation with grace period, revocation, IP allowlist, `expires_at` — see the canonical [Authentication guide](/guides/authentication). Keys are shared across the REST and MCP surfaces. ### OAuth 2.1 [#oauth-21] For third-party apps, Factuarea is a full **OAuth 2.1 Authorization Server**. It supports Dynamic Client Registration, the authorization-code flow with PKCE, and refresh-token rotation. No pre-registration or manual app approval is required — a client registers itself and the user authorizes it. (Interactive clients like Claude Code and the MCP Inspector drive this whole flow for you; the steps below are for building your own client.) #### Discovery [#discovery] Clients discover the server's capabilities through standard metadata endpoints (no `/api` prefix): | Endpoint | RFC | Purpose | | ----------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/.well-known/oauth-authorization-server` | [8414](https://www.rfc-editor.org/rfc/rfc8414) | Authorization Server Metadata — lists the authorize/token/register/introspect/revoke endpoints, supported scopes, `code_challenge_methods_supported: ["S256"]`. | | `/.well-known/oauth-protected-resource` | [9728](https://www.rfc-editor.org/rfc/rfc9728) | Protected Resource Metadata — declares the MCP resource and which authorization server issues valid tokens. | When an unauthenticated request hits the endpoint, the server responds `401` with a `WWW-Authenticate: Bearer ..., resource_metadata="<url>"` header so RFC 9728 clients can find the authorization server without guessing. #### 1. Dynamic Client Registration (RFC 7591) [#1-dynamic-client-registration-rfc-7591] A client registers itself by POSTing its metadata; the server returns a `client_id` (and a `client_secret` for confidential clients): ```bash curl -X POST https://mcp.factuarea.com/api/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "My Invoicing Assistant", "redirect_uris": ["https://myapp.example.com/callback"], "token_endpoint_auth_method": "none" }' ``` Public clients (browser/native apps) register with `token_endpoint_auth_method: "none"` and rely on PKCE; confidential clients use `client_secret_basic`. Registration is rate-limited to **60 per minute per IP**. #### 2. Authorization with PKCE [#2-authorization-with-pkce] Send the user to the authorize endpoint with a PKCE challenge (`code_challenge_method=S256` is the only method accepted): ``` GET https://mcp.factuarea.com/api/oauth/authorize ?response_type=code &client_id=<client_id> &redirect_uri=https://myapp.example.com/callback &scope=factuarea.read invoices.write &state=<opaque> &code_challenge=<base64url(sha256(verifier))> &code_challenge_method=S256 ``` This renders the **consent screen**, where the user: 1. Picks the **company** to grant access to (a user may belong to several). 2. Picks the **environment** — **live** (the real company) or **test** (an isolated sandbox), Stripe-style. Test is opt-in; absent ⇒ live. 3. Reviews and **selects the scopes** to grant. Sensitive scopes are flagged and not pre-checked. On approval the server redirects back with a single-use `code` (and your `state`). Sensitive operations are filtered out of the catalog the user can approve — see the [channel policy](#channel-policy). #### 3. Token exchange [#3-token-exchange] Exchange the code for an access token, sending the PKCE verifier: ```bash curl -X POST https://mcp.factuarea.com/api/oauth/token \ -d grant_type=authorization_code \ -d code=<code> \ -d redirect_uri=https://myapp.example.com/callback \ -d client_id=<client_id> \ -d code_verifier=<verifier> ``` ```json { "access_token": "<opaque>", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "<opaque>", "scope": "profile.read clients.read invoices.read invoices.write" } ``` The token persists the **expanded, fine-grained** scopes (macros like `factuarea.read` are expanded at issue time). Use the access token as the Bearer credential. The token endpoint is rate-limited to **60 per minute per (client, IP)** and requires client authentication (HTTP Basic for confidential clients, `client_id` in the body for public ones). #### 4. Refresh-token rotation [#4-refresh-token-rotation] Refresh tokens **rotate by family**: each refresh issues a new access token and a new refresh token, and invalidates the one you used. ```bash curl -X POST https://mcp.factuarea.com/api/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=<refresh_token> \ -d client_id=<client_id> ``` If a refresh token is **replayed** (used after rotation — the classic sign of a leak), the server detects the reuse, revokes the entire token family and raises a security alert. Always store and use the latest refresh token only. #### Revocation & introspection [#revocation--introspection] | Endpoint | RFC | Purpose | | ---------------------------- | ---------------------------------------------- | ------------------------------------------------------------- | | `POST /api/oauth/revoke` | [7009](https://www.rfc-editor.org/rfc/rfc7009) | Revoke an access or refresh token. | | `POST /api/oauth/introspect` | [7662](https://www.rfc-editor.org/rfc/rfc7662) | Check whether a token is active and read its scopes/metadata. | Both require client authentication. Users can also review and revoke connected apps from the Factuarea dashboard, and a company admin who loses access has their tokens revoked automatically on the next call. ## Test mode [#test-mode] The MCP server runs against the same two **environments** as the REST API — **live** (your real company) and **test** (an isolated sandbox) — so you can build and validate an agent integration without touching production data, the AEAT, or your clients' inboxes. How you select test mode depends on the channel: | Channel | How to use test mode | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | **API key** | Authenticate with a `fact_test_` key. The prefix is the source of truth — a `fact_test_` token always operates on the sandbox. | | **OAuth 2.1** | On the consent screen, pick the **Test** environment (Stripe-style). Absent ⇒ **live**. The issued token is bound to that environment. | A test credential operates on a dedicated **sandbox company** — a technical twin of your real company, provisioned automatically and inheriting its plan, so module/plan gating behaves faithfully. Isolation is **structural** (test and live data live in separate companies), and external effects are switched off: | Effect | In `live` | In `test` | | ------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | **VeriFactu** | The Alta record is created and transmitted to the AEAT. | Created **locally**, but **never transmitted** to the AEAT. | | **Email** | Document emails reach real recipients. | **Not delivered** to real recipients. | | **Webhooks** | Subscribed events are delivered to your endpoints. | Recorded with `livemode: false`, but **not delivered**. | | **FACe (FacturaE)** | Submissions are presented to the real FACe web service. | **Simulated** — no SOAP call leaves Factuarea; the registry number is synthetic (`FACE-SANDBOX-*`). | Everything else behaves exactly as in production, and the full set of <Stat n="tools" /> tools is available in both environments (subject to your scopes and plan). When your flow works end-to-end, switch to live: create a `fact_live_` key, or re-run the consent flow and select the **live** environment. <Callout type="info"> This is the same sandbox mechanism as the REST API. See the canonical [Test mode & sandbox](/guides/test-mode) guide for how the sandbox company is provisioned and queried. </Callout> --- # Errors & rate limits (/mcp/errors) The MCP server speaks strict **JSON-RPC 2.0**. Failures come back as a `error` object, never as an HTTP error body the way the REST API does — but the **semantics are identical**: the same business-rule violation that returns `422` over REST returns the equivalent JSON-RPC error here, with the v1 `code` and `http_status` preserved in `data`. This page covers the MCP-specific JSON-RPC mapping and the MCP throttling buckets. For the canonical REST contract — the error envelope by `code`, and the per-tier quotas — see [Errors](/guides/errors) and [Rate limits](/guides/rate-limits). ## Error shape [#error-shape] ```json { "jsonrpc": "2.0", "id": "<request id>", "error": { "code": -32008, "message": "invoice_cannot_be_modified", "data": { "http_status": 422, "code": "invoice_cannot_be_modified", "hint": "La factura ya emitida no puede modificarse.", "param": "status" } } } ``` * **`error.code`** — the JSON-RPC numeric code (always in the `-32099..-32000` implementation-defined range, or `-32603` for internal errors). * **`error.message`** — a stable string identifier (e.g. `insufficient_scope`, `invoice_cannot_be_modified`). * **`error.data.code`** — the same canonical v1 `code` the REST API returns, so you can branch on one value across both surfaces. * **`error.data.http_status`** — the HTTP status the equivalent REST call would return (422 / 404 / 409 / …), for clients that prefer to reason in HTTP terms. * **`error.data.hint`** — a human-readable message (in Spanish, matching the app's locale). Other fields (`param`, `subcode`, `required_scope`, …) appear when relevant. ## Code table [#code-table] | JSON-RPC code | `message` / `data.code` | HTTP equiv. | Meaning | | ------------- | --------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-32001` | `invalid_token` | 401 | Missing, malformed or unknown credential; or the user is no longer a member of the company. | | `-32002` | `client_revoked` | 403 | The OAuth client was revoked. | | `-32003` | `invalid_token_type` | 403 | Wrong credential type for this surface. | | `-32004` | `plan_limit_exceeded` / `plan_upgrade_required` | 402 | A plan usage limit was hit, or the action needs a higher plan. `data` carries `resource`, `current`, `limit`. | | `-32005` | `insufficient_scope` / `module_not_in_plan` / `feature_flag_disabled` | 403 | The credential lacks the required scope, the module isn't in the plan, or a feature flag is off. `data` carries `required_scope` / `module` / `flag`. | | `-32006` | `rate_limit_exceeded` | 429 | A throttle bucket was exceeded. `data` carries `retry_after` and `bucket`; the response also sets the `Retry-After` header. | | `-32007` | `addon_not_active` | 403 | The company has no active Factuarea plan that includes public API access (e.g. an expired trial or lapsed subscription outside its grace period). | | `-32008` | *(v1 code)* | 422 / 404 / 409 / … | A business-rule violation, missing resource or conflict. `message` and `data.code` are the canonical v1 error code; `data.http_status` tells you the category. | | `-32603` | `internal_error` | 500 | Unexpected server error. | <Callout type="info"> `-32005` and `-32008` each cover several sub-causes. Always branch on `data.code` (the string), not only on the numeric `code`, when you need to tell them apart — e.g. `insufficient_scope` vs `module_not_in_plan` both surface as `-32005`. </Callout> ### Insufficient scope [#insufficient-scope] When a tool needs a scope the credential doesn't hold: ```json { "jsonrpc": "2.0", "id": "req-42", "error": { "code": -32005, "message": "insufficient_scope", "data": { "http_status": 403, "code": "insufficient_scope", "required_scope": "invoices:write", "provided_scopes": ["invoices:read", "clients:read"], "hint": "La credencial no tiene el scope requerido para esta operación." } } } ``` Tools your credential can't reach are also **hidden** from `tools/list`, so a well-behaved agent won't normally attempt them — this error is the safety net. ## Rate limits [#rate-limits] Requests are throttled across three independent buckets. Exceeding any one returns `-32006` with a `Retry-After` header (seconds). ### Per-token, by tool category [#per-token-by-tool-category] Each credential has separate per-minute counters per tool **category**, so heavy destructive use can't starve your reads: | Category | Default limit | Example tools | | ---------------- | ------------- | --------------------------------------- | | `read` / `write` | 60 / min | `search_invoices`, `create_invoice` | | `send` | 20 / min | `send_invoice`, `send_quote` | | `generate` | 30 / min | `get_invoice_facturae_link` | | `destructive` | 10 / min | `delete_invoice`, `bulk_delete_clients` | The bucket is resolved from the tool's [category](/mcp/tools#how-to-read-this-catalog). The counter is incremented **before** the tool runs, so rejected calls (bad scope, validation error) still consume quota — this is deliberate anti-abuse, matching the standard OAuth/REST pattern. ### Per-plan, hourly [#per-plan-hourly] A company-wide hourly cap by plan slug. The **Enterprise** plan bypasses this bucket entirely. ### Per-OAuth-client, global [#per-oauth-client-global] OAuth apps additionally share a global per-client bucket of **1000 / min**, so a single misbehaving app can't overwhelm the server across all its users. ### Rate-limit headers [#rate-limit-headers] Successful responses carry the remaining budget so you can back off proactively: | Header | Meaning | | --------------------------------------------------------- | -------------------------------------------------- | | `X-RateLimit-Limit-Token` / `X-RateLimit-Remaining-Token` | The per-token (per-category) bucket. | | `X-RateLimit-Limit-Hour` / `X-RateLimit-Remaining-Hour` | The per-plan hourly bucket (absent on Enterprise). | | `Retry-After` | On a `429`, seconds to wait before retrying. | ### Auth endpoint limits [#auth-endpoint-limits] The OAuth endpoints have their own limits, independent of the MCP buckets: | Endpoint | Limit | | -------------------------- | ------------------------- | | `POST /api/oauth/register` | 10 / hour per IP | | `POST /api/oauth/token` | 60 / min per (client, IP) | <Callout type="warn"> Always honor `Retry-After`. Retrying before it elapses keeps the bucket full and only delays your recovery. Combine it with idempotency on writes so a delayed retry never duplicates a document. </Callout> --- # Scopes & permissions (/mcp/scopes) Every MCP tool declares the **scope** a credential must hold to call it. Scopes work slightly differently per channel: * **API keys** are created directly with **fine-grained** scopes (`resource:action`, e.g. `invoices:read`) — the same closed catalog the REST API uses. You can also grant the super-scope `*`. * **OAuth tokens** are granted **dotted** scopes (`resource.action`, e.g. `invoices.read`) on the consent screen. The server translates these to the fine-grained scopes automatically, so both channels enforce the same set at the tool boundary. ## OAuth consent catalog [#oauth-consent-catalog] These are the scopes a user can grant a third-party app on the consent screen. There are **59 simple scopes** plus **3 macros**. ### Simple scopes [#simple-scopes] Each grants one capability. The **Maps to** column shows the fine-grained scope the tools enforce — the consent layer translates dotted OAuth scopes to these automatically. The **Sensitive** column marks scopes the consent screen flags and does not pre-check. #### Profile [#profile] | Scope | Grants | Maps to | Sensitive | | -------------- | ----------------------------------------- | -------------- | --------- | | `profile.read` | Read your name, email and active company. | `account:read` | no | #### CRM — clients & suppliers [#crm--clients--suppliers] | Scope | Grants | Maps to | Sensitive | | ------------------ | ---------------------------- | ------------------ | --------- | | `clients.read` | List and read clients. | `clients:read` | no | | `clients.write` | Create and update clients. | `clients:write` | no | | `clients.delete` | Delete clients. | `clients:delete` | ⚠ yes | | `suppliers.read` | List and read suppliers. | `suppliers:read` | no | | `suppliers.write` | Create and update suppliers. | `suppliers:write` | no | | `suppliers.delete` | Delete suppliers. | `suppliers:delete` | ⚠ yes | #### Catalog — products, series, taxes [#catalog--products-series-taxes] | Scope | Grants | Maps to | Sensitive | | ----------------- | ----------------------------------- | ----------------- | --------- | | `products.read` | List and read the product catalog. | `products:read` | no | | `products.write` | Create and update products. | `products:write` | no | | `products.delete` | Delete products. | `products:delete` | ⚠ yes | | `series.read` | Read numbering series. | `series:read` | no | | `series.write` | Create and update numbering series. | `series:write` | no | | `taxes.read` | Read tax rates and retentions. | `taxes:read` | no | | `taxes.write` | Create and update tax rates. | `taxes:write` | no | #### Sales — invoices, quotes, pro-formas, delivery notes [#sales--invoices-quotes-pro-formas-delivery-notes] | Scope | Grants | Maps to | Sensitive | | ---------------------------- | --------------------------------------------- | --------------------------- | --------- | | `invoices.read` | List and read invoices. | `invoices:read` | no | | `invoices.write` | Create and update invoices. | `invoices:write` | no | | `invoices.send` | Send invoices by email. | `invoices:send` | no | | `invoices.delete` | Delete draft invoices. | `invoices:delete` | ⚠ yes | | `invoices.annul` | Annul issued invoices. | `invoices:void` | ⚠ yes | | `invoices.create_corrective` | Issue corrective invoices. | `invoices:write` | no | | `quotes.read` | List and read quotes. | `quotes:read` | no | | `quotes.write` | Create and update quotes. | `quotes:write` | no | | `quotes.send` | Send quotes by email. | `quotes:send` | no | | `quotes.delete` | Delete quotes. | `quotes:delete` | ⚠ yes | | `quotes.convert_to_invoice` | Accept/reject and convert quotes to invoices. | `quotes:transition` | no | | `proformas.read` | List and read pro-forma invoices. | `proformas:read` | no | | `proformas.write` | Create and update pro-formas. | `proformas:write` | no | | `proformas.send` | Send pro-formas by email. | `proformas:send` | no | | `proformas.delete` | Delete pro-formas. | `proformas:delete` | ⚠ yes | | `proformas.convert` | Convert pro-formas to invoices. | `proformas:transition` | no | | `delivery_notes.read` | List and read delivery notes. | `delivery_notes:read` | no | | `delivery_notes.write` | Create, update and send delivery notes. | `delivery_notes:write` | no | | `delivery_notes.send` | Send delivery notes by email. | `delivery_notes:write` | no | | `delivery_notes.delete` | Delete delivery notes. | `delivery_notes:delete` | ⚠ yes | | `delivery_notes.convert` | Convert delivery notes. | `delivery_notes:transition` | no | | `delivery_notes.sign` | Mark delivered / sign delivery notes. | `delivery_notes:transition` | ⚠ yes | #### Purchases [#purchases] | Scope | Grants | Maps to | Sensitive | | ----------------------------- | ------------------------------- | ------------------------------ | --------- | | `purchase_invoices.read` | List and read vendor bills. | `purchase_invoices:read` | no | | `purchase_invoices.write` | Create and update vendor bills. | `purchase_invoices:write` | no | | `purchase_invoices.mark_paid` | Mark vendor bills as paid. | `purchase_invoices:transition` | ⚠ yes | | `purchase_invoices.delete` | Delete vendor bills. | `purchase_invoices:delete` | ⚠ yes | <Callout type="info"> **Payment scopes are asymmetric across sales and purchases.** Registering a payment on a **sales** invoice (`register_invoice_payment`) needs `invoices:write` — it edits the invoice. Registering a payment on a **purchase** invoice (`register_purchase_invoice_payment`) needs `purchase_invoices:transition` instead, because on the buy side a payment moves the bill through its lifecycle (pending → paid) rather than editing it. </Callout> #### Recurring invoices [#recurring-invoices] | Scope | Grants | Maps to | Sensitive | | ------------------------ | -------------------------------------- | ------------------------------- | --------- | | `recurring.read` | List and read recurring templates. | `recurring_invoices:read` | no | | `recurring.write` | Create and update recurring templates. | `recurring_invoices:write` | no | | `recurring.pause` | Pause recurring templates. | `recurring_invoices:transition` | no | | `recurring.resume` | Resume recurring templates. | `recurring_invoices:transition` | no | | `recurring.generate_now` | Emit a recurring invoice manually. | `recurring_invoices:transition` | ⚠ yes | | `recurring.delete` | Delete recurring templates. | `recurring_invoices:delete` | ⚠ yes | #### Compliance (VeriFactu) [#compliance-verifactu] | Scope | Grants | Maps to | Sensitive | | ---------------- | -------------------------------------------------------- | ---------------- | --------- | | `verifactu.read` | Read VeriFactu records, events, certificates and config. | `verifactu:read` | no | #### Webhooks [#webhooks] | Scope | Grants | Maps to | Sensitive | | ----------------- | -------------------------------------------------- | ----------------- | --------- | | `webhooks.read` | List webhook endpoints and deliveries. | `webhooks:read` | no | | `webhooks.write` | Create, update, rotate and ping webhook endpoints. | `webhooks:write` | ⚠ yes | | `webhooks.delete` | Delete webhook endpoints. | `webhooks:delete` | ⚠ yes | #### Workforce — control horario [#workforce--control-horario] Employee, time-tracking, absence, work-schedule, presence, holiday and payroll-export data. Every workforce scope is **sensitive** (employee PII and compliance data) and requires the `control_horario` plan module — see [Plan & module gating](#plan--module-gating). Reads, `employees.write` and payroll-export generation are grantable on the consent screen; the privileged write and transition actions have no OAuth dotted scope and are API-key-only (listed under the fine-grained scopes below). | Scope | Grants | Maps to | Sensitive | | ----------------------- | ---------------------------------------------------------- | ----------------------- | --------- | | `employees.read` | List and read employees. | `employees:read` | ⚠ yes | | `employees.write` | Create and update employees. | `employees:write` | ⚠ yes | | `time_entries.read` | Read time-clock entries, balances and monthly time sheets. | `time_entries:read` | ⚠ yes | | `absences.read` | List and read absences, policies and requests. | `absences:read` | ⚠ yes | | `work_schedules.read` | Read work schedules and their assignments. | `work_schedules:read` | ⚠ yes | | `presence.read` | Read live and daily presence. | `presence:read` | ⚠ yes | | `holidays.read` | Read the company holiday calendar. | `holidays:read` | ⚠ yes | | `payroll_exports.read` | Read generated payroll exports. | `payroll_exports:read` | ⚠ yes | | `payroll_exports.write` | Generate payroll exports. | `payroll_exports:write` | ⚠ yes | ### Macro scopes [#macro-scopes] Convenience bundles that expand to a list of simple scopes at token-issue time. The token persists the **expanded** scopes — macros are never stored. | Macro | Grants | Sensitive | | ----------------- | ------------------------------------------------------------------------------------------------------ | --------- | | `factuarea.read` | Full read access to everything (no writes). | no | | `factuarea.write` | Read everything, plus create/update documents and send emails. | no | | `factuarea.full` | Read, write, send and destructive actions (delete, annul, mark paid, sign). Excludes VeriFactu writes. | ⚠ yes | ## The super-scope `*` [#the-super-scope-] A credential holding `*` covers **every** scope — all <Stat n="tools" /> tools for an API key. It's the equivalent of an owner key. Reserve it for one-off migrations or fully-trusted owner automations; prefer the narrowest scope set for everything else. The super-scope is available to API keys; OAuth consent grants explicit scopes (or macros), never a raw `*`. ## How OAuth scopes become fine scopes [#how-oauth-scopes-become-fine-scopes] When an OAuth token is issued, its dotted scopes are translated once to the fine-grained catalog the tools enforce. A few reconciliations are worth knowing: * `recurring.*` maps to the `recurring_invoices:*` resource. * `invoices.create_corrective` maps to `invoices:write` (creating is a write). * `invoices.annul` maps to `invoices:void`. * Lifecycle actions (`*.convert`, `*.sign`, `*.pause`, `*.resume`, `*.generate_now`, `*.mark_paid`, `quotes.convert_to_invoice`) map to the resource's `:transition` scope. * Any read scope on a document also grants the transversal read utilities `pdfs:read` (download its PDF/receipt) and `events:read` (its activity log). * `verifactu.write` and `delivery_notes:gdpr_forget` have **no** OAuth dotted scope — they are unreachable via OAuth by design. * `facturae:read` / `facturae:write` are **not in the OAuth consent catalog yet** — the FacturaE (FACe) tools are reachable only with an API key for now. ## Fine-grained scopes without an OAuth scope (API key only) [#fine-grained-scopes-without-an-oauth-scope-api-key-only] A few fine-grained scopes live in the closed `resource:action` catalog API keys use, but have **no dotted OAuth equivalent** — they are never granted through a third-party consent screen and are reachable only with an API key. Grant them on the key directly (or via the super-scope `*`). Some are gated by an integration module — the key's company must then hold the corresponding plan (see [Plan & module gating](#plan--module-gating)); the rest are first-party account and gestoría scopes. | Scope | Grants | Module gate | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `stripe_autoinvoicing:read` | Read the Stripe Connect integration status, auto-invoicing config and connected accounts, and list auto-invoiced charges/correctives. | `integration_stripe` | | `stripe_autoinvoicing:write` | Toggle Stripe charge auto-invoicing, set the auto-issued series, and edit/disconnect connected accounts. | `integration_stripe` | | `payouts:read` | Read ingested Stripe payouts and their bank-reconciliation status. | `integration_stripe` | These power the [Payments & gateways tools](/mcp/tools#payments). A second group of API-key-only scopes governs **first-party** account and **gestoría** management — your own credentials and, for accountancies, the child sub-companies you manage and their API keys. `companies:*` requires the gestoría plan module; the rest have no module gate. | Scope | Grants | Module gate | | ------------------ | ------------------------------------------------------------------------------------- | ----------- | | `account:write` | Manage your own API keys (create, rotate, revoke) and update account personalization. | — | | `companies:read` | List and read managed companies (child sub-accounts). | `gestoria` | | `companies:write` | Create, update, activate and deactivate managed companies. | `gestoria` | | `companies:delete` | Archive managed companies. | `gestoria` | | `api_keys:read` | List and read API keys of managed companies. | — | | `api_keys:write` | Create, rotate and revoke API keys of managed companies. | — | | `api_keys:delete` | Permanently delete API keys of managed companies. | — | A third group covers **workforce** (control horario) write and transition actions. Their read counterparts are OAuth-grantable (see the Workforce consent scopes above), but these privileged scopes have no OAuth dotted equivalent — they are API-key-only, the mirror of `verifactu:write`. All require the `control_horario` plan module. | Scope | Grants | Module gate | | ---------------------- | -------------------------------------------------------------------------------------------- | ----------------- | | `employees:delete` | Permanently delete employees. | `control_horario` | | `time_entries:write` | Clock in/out, record manual entries, manage time corrections and the monthly register close. | `control_horario` | | `absences:write` | Create and manage absence types, policies and requests. | `control_horario` | | `absences:transition` | Approve, reject and cancel absence requests. | `control_horario` | | `work_schedules:write` | Create, update, assign and archive work schedules. | `control_horario` | <Callout> `verifactu:write`, `facturae:read`, `facturae:write` and `delivery_notes:gdpr_forget` are also fine-grained, API-key-only scopes (covered above) — they enforce at the tool boundary just like any other scope, but have no OAuth consent counterpart. </Callout> ## Plan & module gating [#plan--module-gating] Most published tools apply only a **scope** check — they're reachable as soon as the credential holds the required scope. Two families are also **module-gated**. The [Payments & gateways tools](/mcp/tools#payments) map their scopes (`stripe_autoinvoicing:*`, `payouts:read`) to the `integration_stripe` module. The workforce tools (Employees, Employee seats, Work schedules, Time tracking, Absences, Presence, Holidays) map their scopes (`employees:*`, `time_entries:*`, `absences:*`, `work_schedules:*`, `presence:read`, `holidays:read`, `payroll_exports:*`) to the `control_horario` module. When the company's plan lacks the module, the server hides those tools from `tools/list` and returns `module_not_in_plan` (`-32005`) on a direct call. * Plan **usage limits** (e.g. monthly document quotas) are enforced at call time and surface as `plan_limit_exceeded` (`-32004`). See [Errors & rate limits](/mcp/errors). The whole public MCP surface also requires the company to have an **active Factuarea plan** — API access is included in every plan; otherwise every call returns `addon_not_active` (`-32007`). --- # Tool catalog (/mcp/tools) The Factuarea MCP server publishes **<Stat n="tools" /> tools** across 27 domains. This page is the canonical list; each tool's input schema is discoverable at runtime via `tools/list`. Every tool maps to the same operation in the REST API Reference. ## How to read this catalog [#how-to-read-this-catalog] * **Tool** — the MCP tool name your agent calls. * **Scope** — the fine-grained scope the credential must hold for the tool to appear in `tools/list` and to be invocable. See [Scopes & permissions](/mcp/scopes). * **Category** — the rate-limit bucket the tool falls into: `read`, `write`, `destructive`, `send` or `generate`. See [Errors & rate limits](/mcp/errors). A credential only sees the tools its scopes cover. An API key with the super-scope `*` sees all <Stat n="tools" />; a narrower key or an OAuth token sees a subset. <Callout type="warn"> **¹ OAuth-restricted (API key only).** <Stat n="oauth_restricted" /> tools are reachable **only** with an API key, never through a third-party OAuth app: the 8 `verifactu:write` tools, `forget_delivery_note_signature` (`delivery_notes:gdpr_forget`), the 5 FacturaE tools (the `facturae:read` / `facturae:write` scopes are not in the OAuth consent catalog yet), the 13 Payments & gateways tools (the `stripe_autoinvoicing:*`, `payouts:read` and `integration_events:*` scopes are fine-grained, API-key-only scopes), the 3 Emails tools and the 2 API-request-log tools (`emails:read` and `developers:read` inspect your own integration's traffic — first-party scopes, never granted by third-party consent), the 17 Managed-companies tools (`companies:*` and `api_keys:*` are first-party only, never granted by third-party consent — this includes `get_consolidated_workforce`), the 4 account-write self-service tools (`account:write` — creating, rotating or revoking API keys and updating account personalization) and the 33 workforce control-horario tools whose write/transition scopes (`time_entries:write`, `absences:write`, `absences:transition`, `work_schedules:write`) are fine-grained, API-key-only scopes not in the OAuth consent catalog (their read counterparts and `employees:write` are OAuth-grantable). OAuth apps therefore reach **<Stat n="oauth_reachable" />** of the <Stat n="tools" /> tools. See the [channel policy](/mcp/connect#channel-policy). </Callout> ## Domains [#domains] ### Invoices [#invoice] Sales invoices: search, CRUD, correctives, partial-payment ledger, transitions (paid/sent/void/annul), scheduling, bulk operations, reminders, public links, PDFs and Excel export. *(43 tools)* | Tool | Scope | Category | | -------------------------------------- | ----------------- | ----------- | | `can_annul_invoice` | `invoices:read` | read | | `check_invoice_simplified_eligibility` | `invoices:read` | read | | `export_invoices_excel` | `invoices:read` | read | | `find_invoice_by_external_id` | `invoices:read` | read | | `find_invoice_by_number` | `invoices:read` | read | | `get_available_quarters` | `invoices:read` | read | | `get_invoice` | `invoices:read` | read | | `get_invoice_activities` | `invoices:read` | read | | `get_invoice_correctives` | `invoices:read` | read | | `get_invoice_payment_receipt` | `pdfs:read` | read | | `get_invoice_pdf` | `pdfs:read` | read | | `get_invoice_public_link` | `invoices:read` | read | | `get_invoice_stats` | `invoices:read` | read | | `get_invoice_statuses` | `invoices:read` | read | | `list_invoice_payments` | `invoices:read` | read | | `list_payment_methods` | `invoices:read` | read | | `preview_invoice_reminder` | `invoices:read` | read | | `search_invoices` | `invoices:read` | read | | `annul_invoice` | `invoices:void` | write | | `assign_invoice_real_number` | `invoices:write` | write | | `bulk_change_invoice_status` | `invoices:write` | write | | `bulk_create_invoices` | `invoices:write` | write | | `bulk_delete_invoices` | `invoices:delete` | destructive | | `bulk_invoices_pdf_link` | `pdfs:read` | generate | | `bulk_send_invoices` | `invoices:send` | send | | `create_corrective_invoice` | `invoices:write` | write | | `create_invoice` | `invoices:write` | write | | `delete_invoice` | `invoices:delete` | destructive | | `duplicate_invoice` | `invoices:write` | write | | `mark_invoice_as_paid` | `invoices:write` | write | | `mark_invoice_as_sent` | `invoices:write` | write | | `mark_invoice_unsent` | `invoices:write` | write | | `quarterly_send_email` | `invoices:send` | send | | `register_invoice_payment` | `invoices:write` | write | | `reschedule_invoice` | `invoices:write` | write | | `schedule_invoice` | `invoices:write` | write | | `send_invoice` | `invoices:send` | send | | `send_invoice_reminder` | `invoices:send` | send | | `substitute_simplified_invoice` | `invoices:write` | write | | `unschedule_invoice` | `invoices:write` | write | | `update_invoice` | `invoices:write` | write | | `update_invoice_public_link` | `invoices:write` | write | | `void_invoice` | `invoices:void` | write | ### Clients [#client] Customer CRM: search, CRUD, bulk create/delete, lookup by tax ID or external ID, CSV import, AEAT census verification, stats and activity. *(13 tools)* | Tool | Scope | Category | | ---------------------------- | ---------------- | ----------- | | `find_client_by_external_id` | `clients:read` | read | | `find_client_by_tax_id` | `clients:read` | read | | `get_client` | `clients:read` | read | | `get_client_activities` | `clients:read` | read | | `get_client_stats` | `clients:read` | read | | `search_clients` | `clients:read` | read | | `verify_client_census` | `clients:read` | read | | `bulk_create_clients` | `clients:write` | write | | `bulk_delete_clients` | `clients:delete` | destructive | | `create_client` | `clients:write` | write | | `delete_client` | `clients:delete` | destructive | | `import_clients_csv` | `clients:write` | write | | `update_client` | `clients:write` | write | ### Suppliers [#supplier] Vendor CRM: search, CRUD, bulk delete/status, lookup by tax ID or external ID, activate/deactivate, stats and activity. *(12 tools)* | Tool | Scope | Category | | ------------------------------ | ------------------ | ----------- | | `find_supplier_by_external_id` | `suppliers:read` | read | | `find_supplier_by_tax_id` | `suppliers:read` | read | | `get_supplier` | `suppliers:read` | read | | `get_supplier_activities` | `suppliers:read` | read | | `get_supplier_stats` | `suppliers:read` | read | | `search_suppliers` | `suppliers:read` | read | | `bulk_change_supplier_status` | `suppliers:write` | write | | `bulk_delete_suppliers` | `suppliers:delete` | destructive | | `create_supplier` | `suppliers:write` | write | | `delete_supplier` | `suppliers:delete` | destructive | | `toggle_supplier_active` | `suppliers:write` | write | | `update_supplier` | `suppliers:write` | write | ### Products [#product] Catalog: search, CRUD, bulk stock/delete/status, SKU or external-ID lookup, sales analytics, low-stock reports and media (gallery/video). *(21 tools)* | Tool | Scope | Category | | -------------------------------- | ----------------- | ----------- | | `download_product_gallery_image` | `products:read` | read | | `download_product_video` | `products:read` | read | | `find_product_by_external_id` | `products:read` | read | | `find_product_by_sku` | `products:read` | read | | `get_product` | `products:read` | read | | `get_product_activities` | `products:read` | read | | `get_product_sales_analytics` | `products:read` | read | | `get_product_stats` | `products:read` | read | | `low_stock_report` | `products:read` | read | | `search_products` | `products:read` | read | | `bulk_change_product_status` | `products:write` | write | | `bulk_delete_products` | `products:delete` | destructive | | `bulk_update_stock` | `products:write` | write | | `create_product` | `products:write` | write | | `delete_product` | `products:delete` | destructive | | `delete_product_gallery_image` | `products:delete` | destructive | | `delete_product_video` | `products:delete` | destructive | | `toggle_product_active` | `products:write` | write | | `update_product` | `products:write` | write | | `upload_product_gallery_image` | `products:write` | write | | `upload_product_video` | `products:write` | write | ### Quotes [#quote] Quotes: search, CRUD, accept/reject, send, convert to invoice, bulk operations, lookup by external ID, public links and PDFs. *(20 tools)* | Tool | Scope | Category | | --------------------------- | ------------------- | ----------- | | `find_quote_by_external_id` | `quotes:read` | read | | `get_quote` | `quotes:read` | read | | `get_quote_pdf` | `pdfs:read` | read | | `get_quote_public_link` | `quotes:read` | read | | `get_quote_stats` | `quotes:read` | read | | `get_quote_statuses` | `quotes:read` | read | | `search_quotes` | `quotes:read` | read | | `accept_quote` | `quotes:transition` | write | | `bulk_change_quote_status` | `quotes:transition` | write | | `bulk_delete_quotes` | `quotes:delete` | destructive | | `bulk_quotes_pdf_link` | `pdfs:read` | generate | | `bulk_send_quotes` | `quotes:send` | send | | `convert_quote` | `quotes:transition` | write | | `create_quote` | `quotes:write` | write | | `delete_quote` | `quotes:delete` | destructive | | `duplicate_quote` | `quotes:write` | write | | `reject_quote` | `quotes:transition` | write | | `send_quote` | `quotes:send` | send | | `update_quote` | `quotes:write` | write | | `update_quote_public_link` | `quotes:write` | write | ### Pro-forma invoices [#proforma] Pro-formas: search, CRUD, accept/reject, send, convert, bulk operations, lookup by external ID, public links and PDFs. *(20 tools)* | Tool | Scope | Category | | ------------------------------ | ---------------------- | ----------- | | `find_proforma_by_external_id` | `proformas:read` | read | | `get_proforma` | `proformas:read` | read | | `get_proforma_pdf` | `pdfs:read` | read | | `get_proforma_public_link` | `proformas:read` | read | | `get_proforma_stats` | `proformas:read` | read | | `get_proforma_statuses` | `proformas:read` | read | | `search_proformas` | `proformas:read` | read | | `accept_proforma` | `proformas:transition` | write | | `bulk_change_proforma_status` | `proformas:transition` | write | | `bulk_delete_proformas` | `proformas:delete` | destructive | | `bulk_proformas_pdf_link` | `pdfs:read` | generate | | `bulk_send_proformas` | `proformas:send` | send | | `convert_proforma` | `proformas:transition` | write | | `create_proforma` | `proformas:write` | write | | `delete_proforma` | `proformas:delete` | destructive | | `duplicate_proforma` | `proformas:write` | write | | `reject_proforma` | `proformas:transition` | write | | `send_proforma` | `proformas:send` | send | | `update_proforma` | `proformas:write` | write | | `update_proforma_public_link` | `proformas:write` | write | ### Delivery notes [#delivery-note] Delivery notes: search, CRUD, deliver/cancel/sign, convert, send, bulk operations, lookup by external ID, public links, PDFs and GDPR signature erasure. *(22 tools)* | Tool | Scope | Category | | ----------------------------------- | ------------------------------ | ----------- | | `find_delivery_note_by_external_id` | `delivery_notes:read` | read | | `get_delivery_note` | `delivery_notes:read` | read | | `get_delivery_note_pdf` | `pdfs:read` | read | | `get_delivery_note_public_link` | `delivery_notes:read` | read | | `get_delivery_note_stats` | `delivery_notes:read` | read | | `get_delivery_note_statuses` | `delivery_notes:read` | read | | `search_delivery_notes` | `delivery_notes:read` | read | | `bulk_change_delivery_note_status` | `delivery_notes:transition` | write | | `bulk_delete_delivery_notes` | `delivery_notes:delete` | destructive | | `bulk_delivery_notes_pdf_link` | `pdfs:read` | generate | | `bulk_send_delivery_notes` | `delivery_notes:write` | send | | `cancel_delivery_note` | `delivery_notes:transition` | write | | `convert_delivery_note` | `delivery_notes:transition` | write | | `create_delivery_note` | `delivery_notes:write` | write | | `delete_delivery_note` | `delivery_notes:delete` | destructive | | `duplicate_delivery_note` | `delivery_notes:write` | write | | `forget_delivery_note_signature` | `delivery_notes:gdpr_forget` ¹ | destructive | | `mark_delivery_note_delivered` | `delivery_notes:transition` | write | | `send_delivery_note` | `delivery_notes:write` | send | | `sign_delivery_note` | `delivery_notes:transition` | write | | `update_delivery_note` | `delivery_notes:write` | write | | `update_delivery_note_public_link` | `delivery_notes:write` | write | ### Purchase invoices [#purchase-invoice] Vendor bills: search, CRUD, mark paid, partial-payment ledger, bulk operations, pending/overdue lists, lookup by external ID, file attachments and payment receipts. *(18 tools)* | Tool | Scope | Category | | -------------------------------------- | ------------------------------ | ----------- | | `download_purchase_invoice_file` | `purchase_invoices:read` | read | | `find_purchase_invoice_by_external_id` | `purchase_invoices:read` | read | | `get_purchase_invoice` | `purchase_invoices:read` | read | | `get_purchase_invoice_payment_receipt` | `pdfs:read` | read | | `get_purchase_invoice_stats` | `purchase_invoices:read` | read | | `list_overdue_purchase_invoices` | `purchase_invoices:read` | read | | `list_pending_purchase_invoices` | `purchase_invoices:read` | read | | `list_purchase_invoice_payments` | `purchase_invoices:read` | read | | `search_purchase_invoices` | `purchase_invoices:read` | read | | `attach_purchase_invoice_file` | `purchase_invoices:write` | write | | `bulk_change_purchase_invoice_status` | `purchase_invoices:transition` | write | | `bulk_delete_purchase_invoices` | `purchase_invoices:delete` | destructive | | `create_purchase_invoice` | `purchase_invoices:write` | write | | `delete_purchase_invoice` | `purchase_invoices:delete` | destructive | | `delete_purchase_invoice_file` | `purchase_invoices:write` | write | | `mark_purchase_invoice_paid` | `purchase_invoices:transition` | write | | `register_purchase_invoice_payment` | `purchase_invoices:transition` | write | | `update_purchase_invoice` | `purchase_invoices:write` | write | ### Recurring invoices [#recurring-invoice] Recurring templates: search, CRUD, create from an invoice, activate/pause/resume/cancel/skip, preview, lookup by external ID, logs and activity. *(17 tools)* | Tool | Scope | Category | | --------------------------------------- | ------------------------------- | ----------- | | `find_recurring_invoice_by_external_id` | `recurring_invoices:read` | read | | `get_recurring_invoice` | `recurring_invoices:read` | read | | `get_recurring_invoice_stats` | `recurring_invoices:read` | read | | `list_recurring_invoice_activities` | `recurring_invoices:read` | read | | `list_recurring_invoice_logs` | `recurring_invoices:read` | read | | `preview_recurring_invoice` | `recurring_invoices:read` | read | | `search_recurring_invoices` | `recurring_invoices:read` | read | | `activate_recurring_invoice` | `recurring_invoices:transition` | write | | `bulk_delete_recurring_invoices` | `recurring_invoices:delete` | destructive | | `cancel_recurring_invoice` | `recurring_invoices:transition` | write | | `create_recurring_invoice` | `recurring_invoices:write` | write | | `create_recurring_invoice_from_invoice` | `recurring_invoices:write` | write | | `delete_recurring_invoice` | `recurring_invoices:delete` | destructive | | `pause_recurring_invoice` | `recurring_invoices:transition` | write | | `resume_recurring_invoice` | `recurring_invoices:transition` | write | | `skip_recurring_invoice` | `recurring_invoices:write` | write | | `update_recurring_invoice` | `recurring_invoices:write` | write | ### Series [#series] Numbering series (immutable for fiscal continuity): search, create, archive/unarchive, set default, bootstrap the four default series of a brand-new company, stats and activity. *(12 tools)* | Tool | Scope | Category | | ----------------------------- | -------------- | -------- | | `find_series_by_code` | `series:read` | read | | `get_default_series_for_type` | `series:read` | read | | `get_series` | `series:read` | read | | `get_series_activities` | `series:read` | read | | `get_series_stats` | `series:read` | read | | `list_active_series` | `series:read` | read | | `search_series` | `series:read` | read | | `archive_series` | `series:write` | write | | `bootstrap_series` | `series:write` | write | | `create_series` | `series:write` | write | | `mark_series_as_default` | `series:write` | write | | `unarchive_series` | `series:write` | write | ### Taxes [#tax] Tax rates (global catalog): search, CRUD-lite, defaults, in-use checks, tax/total calculations and the read-only AEAT tax catalog (regimes, exemption causes, IRPF rates and the legal VAT ↔ equivalence-surcharge pairs). *(16 tools)* | Tool | Scope | Category | | ------------------------------- | ------------- | -------- | | `calculate_tax` | `taxes:read` | read | | `calculate_totals` | `taxes:read` | read | | `check_tax_in_use` | `taxes:read` | read | | `get_active_taxes` | `taxes:read` | read | | `get_tax` | `taxes:read` | read | | `get_tax_catalog` | `taxes:read` | read | | `get_tax_defaults_for_document` | `taxes:read` | read | | `get_tax_stats` | `taxes:read` | read | | `get_taxes_by_type` | `taxes:read` | read | | `get_taxes_for_purchases` | `taxes:read` | read | | `get_taxes_for_sales` | `taxes:read` | read | | `search_taxes` | `taxes:read` | read | | `create_tax` | `taxes:write` | write | | `set_tax_as_default` | `taxes:write` | write | | `set_tax_default_for_document` | `taxes:write` | write | | `toggle_tax_active` | `taxes:write` | write | ### VeriFactu [#verifactu] AEAT SIF: records, events, chain validation, retry, subsanación of rejected records, certificates, settings, responsible declaration and AEAT access log. *(27 tools)* | Tool | Scope | Category | | ----------------------------------------- | ------------------- | -------- | | `find_verifactu_record_by_csv` | `verifactu:read` | read | | `find_verifactu_record_by_huella` | `verifactu:read` | read | | `find_verifactu_record_by_invoice_number` | `verifactu:read` | read | | `get_active_company_certificate` | `verifactu:read` | read | | `get_aeat_access_record` | `verifactu:read` | read | | `get_declaracion_responsable` | `verifactu:read` | read | | `get_declaracion_responsable_history` | `verifactu:read` | read | | `get_invoice_verifactu` | `verifactu:read` | read | | `get_verifactu_activities` | `verifactu:read` | read | | `get_verifactu_config` | `verifactu:read` | read | | `get_verifactu_event` | `verifactu:read` | read | | `get_verifactu_event_summary` | `verifactu:read` | read | | `get_verifactu_record` | `verifactu:read` | read | | `get_verifactu_stats` | `verifactu:read` | read | | `list_aeat_access_records` | `verifactu:read` | read | | `list_company_certificates` | `verifactu:read` | read | | `list_verifactu_events` | `verifactu:read` | read | | `search_verifactu_records` | `verifactu:read` | read | | `validate_verifactu_chain` | `verifactu:read` | read | | `activate_company_certificate` | `verifactu:write` ¹ | write | | `create_invoice_verifactu` | `verifactu:write` ¹ | write | | `retry_verifactu_event` | `verifactu:write` ¹ | write | | `retry_verifactu_record` | `verifactu:write` ¹ | write | | `revoke_company_certificate` | `verifactu:write` ¹ | write | | `subsanar_verifactu_record` | `verifactu:write` ¹ | write | | `update_verifactu_settings` | `verifactu:write` ¹ | write | | `upload_company_certificate` | `verifactu:write` ¹ | write | ### FacturaE (FACe) [#facturae] B2G electronic invoicing: download the FacturaE 3.2.2 XML of an invoice and manage its FACe submissions (submit, track, cancel). *(5 tools)* | Tool | Scope | Category | | ------------------------------- | ------------------ | -------- | | `get_face_submission` | `facturae:read` ¹ | read | | `list_invoice_face_submissions` | `facturae:read` ¹ | read | | `cancel_face_submission` | `facturae:write` ¹ | write | | `get_invoice_facturae_link` | `facturae:read` ¹ | generate | | `send_invoice_to_face` | `facturae:write` ¹ | write | ### Webhooks & events [#webhook] Webhook endpoints, deliveries, secret rotation, ping/replay, test events, and the published event catalog. *(13 tools)* | Tool | Scope | Category | | -------------------------- | ----------------- | -------- | | `get_event` | `events:read` | read | | `get_webhook_delivery` | `webhooks:read` | read | | `get_webhook_endpoint` | `webhooks:read` | read | | `list_events` | `events:read` | read | | `list_webhook_deliveries` | `webhooks:read` | read | | `search_webhook_endpoints` | `webhooks:read` | read | | `create_webhook_endpoint` | `webhooks:write` | write | | `delete_webhook_endpoint` | `webhooks:delete` | write | | `ping_webhook_endpoint` | `webhooks:write` | write | | `replay_webhook_delivery` | `webhooks:write` | write | | `rotate_webhook_secret` | `webhooks:write` | write | | `test_webhook_endpoint` | `webhooks:write` | write | | `update_webhook_endpoint` | `webhooks:write` | write | ### Account [#account] Account fiscal identity and personalization: verify the company's registered name + NIF against the AEAT census, read personalization templates and update account personalization. *(<Stat n="domain:Account" /> tools)* | Tool | Scope | Category | | --------------------------------------- | ----------------- | -------- | | `get_account_billing` | `account:read` | read | | `get_account_personalization_templates` | `account:read` | read | | `update_account_personalization` | `account:write` ¹ | write | | `verify_account_census` | `account:read` | write | ### API keys [#api-key] Self-service API keys for your own tenant: list, create, get, rotate the secret and revoke. The secret is returned once, at creation and rotation. *(5 tools)* | Tool | Scope | Category | | ----------------------- | ----------------- | -------- | | `get_api_key` | `account:read` | read | | `list_api_keys` | `account:read` | read | | `create_api_key` | `account:write` ¹ | write | | `revoke_api_key` | `account:write` ¹ | write | | `rotate_api_key_secret` | `account:write` ¹ | write | ### Managed companies [#gestoria] Accountant (gestoría) mode: manage the master tenant's child companies and their child API keys — create/list/show/update/delete, activate/deactivate, seat-charge preview, provisioning status, per-company API keys, and a consolidated workforce-compliance summary across the child companies. *(17 tools)* | Tool | Scope | Category | | --------------------------------- | -------------------- | -------- | | `get_company` | `companies:read` ¹ | read | | `get_company_api_key` | `api_keys:read` ¹ | read | | `get_company_creation_status` | `companies:read` ¹ | read | | `get_company_seat_charge_preview` | `companies:read` ¹ | read | | `get_consolidated_workforce` | `companies:read` ¹ | read | | `list_companies` | `companies:read` ¹ | read | | `list_company_api_keys` | `api_keys:read` ¹ | read | | `activate_companies` | `companies:write` ¹ | write | | `activate_company` | `companies:write` ¹ | write | | `create_company` | `companies:write` ¹ | write | | `create_company_api_key` | `api_keys:write` ¹ | write | | `deactivate_company` | `companies:write` ¹ | write | | `delete_company` | `companies:delete` ¹ | write | | `revoke_company_api_key` | `api_keys:write` ¹ | write | | `rotate_company_api_key_secret` | `api_keys:write` ¹ | write | | `update_company` | `companies:write` ¹ | write | | `verify_company_creation` | `companies:write` ¹ | write | ### Payments & gateways [#payments] Payment-gateway auto-invoicing and the gateway event inbox: Stripe Connect status and config, connected accounts (multi-store), auto-invoiced charges and correctives, Stripe payouts, and the events the gateways sent to Factuarea — what arrived, what it produced and, when it produced nothing, the typed reason why. This is where to look when a charge did not become an invoice. Only Stripe is live today (GoCardless and MONEI are not yet available). These tools are **API-key only** — their scopes are not in the OAuth consent catalog. The Stripe auto-invoicing and payout tools are additionally gated by the `integration_stripe` module (Empresario plan and up); the event-inbox tools are not. *(13 tools)* <Callout type="warn"> `replay_integrations_event` has a real fiscal effect. If the cause that blocked invoicing is now resolved, replaying a parked event **can issue an actual invoice**, with its series number and its VeriFactu submission. It is not a harmless retry: confirm with the user before calling it. It does not duplicate invoices — the replay goes through the same idempotency check as the original attempt. </Callout> | Tool | Scope | Category | | -------------------------------------- | ------------------------------ | -------- | | `get_integrations_event` | `integration_events:read` ¹ | read | | `get_payout` | `payouts:read` ¹ | read | | `get_stripe_autoinvoicing_config` | `stripe_autoinvoicing:read` ¹ | read | | `get_stripe_connected_account` | `stripe_autoinvoicing:read` ¹ | read | | `list_integrations_events` | `integration_events:read` ¹ | read | | `list_stripe_autoinvoiced_correctives` | `stripe_autoinvoicing:read` ¹ | read | | `list_stripe_autoinvoiced_payments` | `stripe_autoinvoicing:read` ¹ | read | | `list_stripe_connected_accounts` | `stripe_autoinvoicing:read` ¹ | read | | `search_payouts` | `payouts:read` ¹ | read | | `disconnect_stripe_connected_account` | `stripe_autoinvoicing:write` ¹ | write | | `replay_integrations_event` | `integration_events:write` ¹ | write | | `update_stripe_autoinvoicing_config` | `stripe_autoinvoicing:write` ¹ | write | | `update_stripe_connected_account` | `stripe_autoinvoicing:write` ¹ | write | ### Emails [#email] Sent-email log: list the emails Factuarea sent on the company's behalf (invoices, quotes, delivery notes, payment reminders), read one by its id, and summarise the outcome for a batch of up to 100 documents in a single call. Use it to answer "did the email for this invoice go out?" and to investigate failed sends. *(3 tools)* <Callout type="warn"> The status describes the **hand-off to the outgoing SMTP server, not actual delivery**. `sent` means the outgoing mail server accepted the message — it can still bounce or land in spam without Factuarea ever knowing. There is no `delivered`, `bounced` or `opened` state: `queued`, `sending`, `sent` and `failed` are the only ones. Never state that the recipient received, opened or read the message. </Callout> | Tool | Scope | Category | | ----------------------- | --------------- | -------- | | `get_emails` | `emails:read` ¹ | read | | `get_emails_indicators` | `emails:read` ¹ | read | | `list_emails` | `emails:read` ¹ | read | ### API request logs [#request-log] Your own integration's traffic against the public v1 API over the last 30 days: list the calls with their method, path, status code, duration, API-key prefix and environment, filtering by errors only or by any of those fields, and read a single one by its `request_id` — the opaque identifier every response returns in its header. Use it to debug an integration: what it called, when, with which status and how long it took. Headers, body and query string are **not** stored and are never returned. *(2 tools)* | Tool | Scope | Category | | ------------------------------ | ------------------- | -------- | | `get_developers_request_logs` | `developers:read` ¹ | read | | `list_developers_request_logs` | `developers:read` ¹ | read | ### Employees [#employee] Workforce roster: search, CRUD-lite (create/update/deactivate/reactivate), stats and the invitation lifecycle (send/resend/cancel and list employee invitations). Gated by the `control_horario` module. *(12 tools)* | Tool | Scope | Category | | ------------------------------ | ----------------- | -------- | | `find_employee_by_external_id` | `employees:read` | read | | `get_employee` | `employees:read` | read | | `get_employee_stats` | `employees:read` | read | | `list_employee_invitations` | `employees:read` | read | | `search_employees` | `employees:read` | read | | `cancel_employee_invitation` | `employees:write` | write | | `create_employee` | `employees:write` | write | | `deactivate_employee` | `employees:write` | write | | `reactivate_employee` | `employees:write` | write | | `resend_employee_invitation` | `employees:write` | write | | `send_employee_invitation` | `employees:write` | write | | `update_employee` | `employees:write` | write | ### Employee seats [#employee-seat] Employee-seat add-on billing: the per-contract seat subscription — preview and read the seat charge and billing, subscribe, change the seat quantity and cancel the add-on. Gated by the `control_horario` module. *(5 tools)* | Tool | Scope | Category | | ------------------------------- | ----------------- | -------- | | `get_employee_seat_billing` | `employees:read` | read | | `preview_employee_seat_charge` | `employees:read` | read | | `cancel_employee_seat_addon` | `employees:write` | write | | `change_employee_seat_quantity` | `employees:write` | write | | `subscribe_employee_seat_addon` | `employees:write` | write | ### Work schedules [#work-schedule] Weekly work schedules and their assignments: search, CRUD, archive/unarchive, assign/unassign to employees, and read an employee's effective schedule. Gated by the `control_horario` module. *(11 tools)* | Tool | Scope | Category | | -------------------------------- | ------------------------ | -------- | | `get_employee_work_schedule` | `work_schedules:read` | read | | `get_work_schedule` | `work_schedules:read` | read | | `get_work_schedule_stats` | `work_schedules:read` | read | | `list_work_schedule_assignments` | `work_schedules:read` | read | | `search_work_schedules` | `work_schedules:read` | read | | `archive_work_schedule` | `work_schedules:write` ¹ | write | | `assign_work_schedule` | `work_schedules:write` ¹ | write | | `create_work_schedule` | `work_schedules:write` ¹ | write | | `unarchive_work_schedule` | `work_schedules:write` ¹ | write | | `unassign_work_schedule` | `work_schedules:write` ¹ | write | | `update_work_schedule` | `work_schedules:write` ¹ | write | ### Time tracking [#time-tracking] Time clock (RD-ley 8/2019): clock in/out with pauses, manual entries, the time-correction workflow, balances and monthly time sheets, the tamper-evident monthly register close (close/reopen/seal, signature, chain validation) and its exports, plus payroll exports. Gated by the `control_horario` module. *(29 tools)* | Tool | Scope | Category | | ----------------------------------- | ---------------------- | -------- | | `export_closed_register` | `time_entries:read` | read | | `get_current_time_entry_session` | `time_entries:read` | read | | `get_employee_time_balance` | `time_entries:read` | read | | `get_monthly_close_report` | `time_entries:read` | read | | `get_monthly_register_signature` | `time_entries:read` | read | | `get_monthly_time_record_close` | `time_entries:read` | read | | `get_monthly_time_sheet` | `time_entries:read` | read | | `get_team_time_balance_summary` | `time_entries:read` | read | | `get_time_correction` | `time_entries:read` | read | | `get_time_entry` | `time_entries:read` | read | | `get_time_tracking_settings` | `time_entries:read` | read | | `search_monthly_time_record_closes` | `time_entries:read` | read | | `search_time_corrections` | `time_entries:read` | read | | `search_time_entries` | `time_entries:read` | read | | `validate_time_record_chain` | `time_entries:read` | read | | `export_payroll` | `payroll_exports:read` | read | | `list_payroll_export_formats` | `payroll_exports:read` | read | | `approve_time_correction` | `time_entries:write` ¹ | write | | `clock_in` | `time_entries:write` ¹ | write | | `clock_out` | `time_entries:write` ¹ | write | | `close_monthly_time_record` | `time_entries:write` ¹ | write | | `pause_time_entry` | `time_entries:write` ¹ | write | | `record_manual_time_entry` | `time_entries:write` ¹ | write | | `reject_time_correction` | `time_entries:write` ¹ | write | | `reopen_monthly_time_record` | `time_entries:write` ¹ | write | | `request_time_correction` | `time_entries:write` ¹ | write | | `resume_time_entry` | `time_entries:write` ¹ | write | | `seal_monthly_time_record` | `time_entries:write` ¹ | write | | `update_time_tracking_settings` | `time_entries:write` ¹ | write | ### Absences [#absence] Absences: types, policies (with carryover configuration and assignments), requests (create/approve/reject/cancel), balances and the team absence calendar. Gated by the `control_horario` module. *(25 tools)* | Tool | Scope | Category | | ------------------------------------ | ----------------------- | -------- | | `get_absence_balance` | `absences:read` | read | | `get_absence_calendar` | `absences:read` | read | | `get_absence_policy` | `absences:read` | read | | `get_absence_request` | `absences:read` | read | | `get_absence_type` | `absences:read` | read | | `list_absence_policy_assignments` | `absences:read` | read | | `search_absence_balances` | `absences:read` | read | | `search_absence_policies` | `absences:read` | read | | `search_absence_requests` | `absences:read` | read | | `search_absence_types` | `absences:read` | read | | `archive_absence_policy` | `absences:write` ¹ | write | | `archive_absence_type` | `absences:write` ¹ | write | | `assign_absence_policy` | `absences:write` ¹ | write | | `configure_absence_policy_carryover` | `absences:write` ¹ | write | | `create_absence_policy` | `absences:write` ¹ | write | | `create_absence_request` | `absences:write` ¹ | write | | `create_absence_type` | `absences:write` ¹ | write | | `unarchive_absence_policy` | `absences:write` ¹ | write | | `unarchive_absence_type` | `absences:write` ¹ | write | | `unassign_absence_policy` | `absences:write` ¹ | write | | `update_absence_policy` | `absences:write` ¹ | write | | `update_absence_type` | `absences:write` ¹ | write | | `approve_absence_request` | `absences:transition` ¹ | write | | `cancel_absence_request` | `absences:transition` ¹ | write | | `reject_absence_request` | `absences:transition` ¹ | write | ### Presence [#presence] Presence: the live presence board and per-employee / daily presence state. Gated by the `control_horario` module. *(3 tools)* | Tool | Scope | Category | | ----------------------- | --------------- | -------- | | `get_employee_presence` | `presence:read` | read | | `get_live_presence` | `presence:read` | read | | `list_daily_presence` | `presence:read` | read | ### Holidays [#holiday] Holidays: read the company holiday calendar and resolve the holidays applicable to an employee's region (CCAA). Gated by the `control_horario` module. *(3 tools)* | Tool | Scope | Category | | ----------------------------- | --------------- | -------- | | `get_holiday` | `holidays:read` | read | | `list_holidays` | `holidays:read` | read | | `resolve_applicable_holidays` | `holidays:read` | read | --- # GoCardless (/payments/gocardless) <Callout type="warn"> **GoCardless is not available yet.** Its v1 endpoints and MCP tools are **not registered**, so calling them today returns `404 route_not_found`. This page documents the **status** of the integration and the surface that will appear when it is released — it is not a usage guide, and nothing below should be read as "you can call this now". </Callout> GoCardless collects by **SEPA Direct Debit**: instead of charging a card, your customer signs a **mandate** authorising you to pull money from their bank account, and every collection afterwards runs against that mandate. That model changes two things compared with a card gateway — money moves on a deferred schedule with a guarantee window, and the mandate has a life of its own that starts, activates, and can be cancelled or expire independently of any single charge. ## What "not released" means [#status] The single source of truth is the backend's list of released payment gateways (`integrations.released_providers`), which today contains **Stripe only**. It is configuration, not code, so a gateway is switched on without a code deploy. While GoCardless is outside that list: * Its **v1 route block is not registered**. `GET /v1/gocardless/mandates` and the `/v1/gocardless-autoinvoicing/*` endpoints do not exist — they are absent from the route registry, from the OpenAPI spec and from the API reference of this site. * Its **MCP tools are filtered out** of the public server, so an agent cannot discover or call them. * The gateway shows as **"Próximamente"** (coming soon) in the integrations marketplace of the dashboard, and the connect flow is blocked at the command handler too — even for a super-admin who bypasses module middleware. * The gateway-agnostic connected-accounts endpoint filters its results to released gateways, so no GoCardless account can surface through it either. Nothing is missing or half-built: the classes are **asleep, not absent**. The release flips one list. ## What already exists behind the flag [#built] | Piece | State | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | OAuth 2 connect flow | Built. GoCardless authenticates with OAuth 2, unlike MONEI | | Webhook signature verification | Built | | Event normaliser | Built — maps GoCardless events onto the same internal payment events the Stripe pipeline uses | | SEPA mandates | Built — stored with their own life cycle: `pending`, `active`, `cancelled`, `expired`, `failed`, kept in sync from the `mandates.*` webhooks | | Per-gateway connected accounts | Built — list, retrieve, update and disconnect, mirroring the Stripe multi-store model | | Auto-invoiced charges and correctives | Built — same decision rules, same VeriFactu registration as Stripe | ### Which events invoice, and which deliberately do not [#events] The normaliser is stricter than "any payment event issues an invoice", and the reason is the SEPA guarantee window: | GoCardless event | What it produces | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `payments.confirmed` | Treated as **paid** → the auto-invoicing flow runs | | `payments.charged_back`, `payments.late_failure` | Treated as a **refund** → corrective invoice flow | | `payments.created`, `payments.submitted` | **Deliberately ignored** — intermediate states of a deferred debit; invoicing before the collection is guaranteed would mean invoicing money that can still bounce | | `payments.paid_out` | No effect today (payout reconciliation for GoCardless is a separate follow-up) | | Anything else | Recorded as an unknown event | That is why a GoCardless charge does not become an invoice the instant it is submitted, and it is the main behavioural difference you will feel coming from Stripe. ## The surface that appears on release [#future-surface] ### v1 endpoints [#future-endpoints] | Endpoint | Scope | | ------------------------------------------------------------------ | -------------------------------- | | `GET /v1/gocardless/mandates` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/connected-accounts` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:read` | | `PUT /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:write` | | `DELETE /v1/gocardless-autoinvoicing/connected-accounts/{account}` | `gocardless_autoinvoicing:write` | | `GET /v1/gocardless-autoinvoicing/payments` | `gocardless_autoinvoicing:read` | | `GET /v1/gocardless-autoinvoicing/correctives` | `gocardless_autoinvoicing:read` | Mandates are **read-only on the public API**: their life cycle is driven by the `mandates.*` webhooks, not by your calls. ### MCP tools [#future-tools] `list_gocardless_mandates`, `list_gocardless_connected_accounts`, `get_gocardless_connected_account`, `update_gocardless_connected_account`, `disconnect_gocardless_connected_account`, `list_gocardless_autoinvoiced_payments` and `list_gocardless_autoinvoiced_correctives` — one per endpoint above, with the same scopes. ### Plan requirement [#plan] The GoCardless integration is a module of the **Empresario** and **Enterprise** plans, like the Stripe and MONEI integrations. Being on the right plan will not be enough on its own while the gateway is unreleased — both conditions have to hold. ## Mapping against the Stripe flow [#stripe-parity] Everything you already know from [Stripe auto-invoicing](/payments/stripe-autoinvoicing) transfers, because the gateway-specific part ends at the normaliser: from there on, both gateways share the same invoicing pipeline, the same fiscal decisions and the same VeriFactu registration. | Concept | Stripe | GoCardless | | ------------------------------ | --------------------------------------------------- | ---------------------------------------------------------------------- | | Authentication | OAuth 2 (Stripe Connect) | OAuth 2 | | Multi-store | `connected-accounts` per account | Same model, under `gocardless-autoinvoicing/connected-accounts` | | "Charge succeeded" signal | `charge.succeeded` / `invoice.paid` | `payments.confirmed` (after the SEPA guarantee window) | | Refunds | `charge.refunded` → corrective invoice | `payments.charged_back` / `payments.late_failure` → corrective invoice | | Mandates | Not applicable | First-class resource with its own life cycle | | Ordinary vs simplified invoice | Same decision rules | Same decision rules | | Subscription cycles | `invoice.paid` with a subscription `billing_reason` | No equivalent branch: the normaliser maps `payments.*` events only | | Payout reconciliation | [Supported](/payments/payouts-reconciliation) | Not covered today | ## What works today regardless [#inbox] The [integration event inbox](/payments/integration-events-inbox) is **gateway-agnostic** and is registered unconditionally. It records events from every integration that writes history, including gateways that are not released yet — because hiding those rows would leave you without an explanation for charges that never got invoiced. `provider=gocardless` is a valid filter value there on day one. --- # Integration event inbox (/payments/integration-events-inbox) A payment gateway sends Factuarea an event for everything that happens on your account: a charge succeeded, a refund was issued, a subscription cycle was billed, a payout landed. Most of those events produce something — an invoice, a corrective invoice, a payment record. Some produce nothing, and when that happens the interesting question is always the same: **why didn't this charge become an invoice?** The **integration event inbox** answers it. Every event Factuarea receives is recorded with what it produced and, when it produced nothing, a **typed discard reason** drawn from a closed catalogue. No guessing from logs, no support ticket: the reason is a value you can filter on, and for the reasons you can act upon it comes with the next step and, sometimes, with the ability to reprocess the event. The inbox is **gateway-agnostic**. It records events from every integration that writes history — including gateways that are not released yet and historical events of one that gets retired — because hiding those rows would leave you without an explanation for charges that never got invoiced. Three endpoints expose it: | Operation | Endpoint | Scope | | --------------------- | --------------------------------------------- | -------------------------- | | List events | `GET /v1/integrations/events` | `integration_events:read` | | Retrieve one event | `GET /v1/integrations/events/{event}` | `integration_events:read` | | Replay a parked event | `POST /v1/integrations/events/{event}/replay` | `integration_events:write` | The same surface exists as MCP tools — `list_integrations_events`, `get_integrations_event` and `replay_integrations_event` — with the same scopes. ## Browsing the inbox [#listing] Newest first, scoped to the authenticated company. Cursor-based pagination with `limit` (1 to 100, defaults to 25) and `starting_after`: ```bash curl -G https://api.factuarea.com/v1/integrations/events \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "provider=stripe" \ --data-urlencode "status=skipped" \ --data-urlencode "limit=50" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f", "object": "integration_event", "provider": "stripe", "event_type": "invoice.paid", "direction": "inbound", "status": "skipped", "discard_reason": "subscription_autoinvoicing_disabled", "discard_reason_label": "Auto-facturación de suscripciones desactivada", "is_actionable": true, "is_replayable": true, "error_message": null, "duration_ms": 412, "created_at": "2026-07-14T09:31:07Z" } ], "has_more": true, "next_cursor": "84120" } ``` Treat `next_cursor` as **opaque**: for this listing it is a numeric string, not a UUID v7 like the cursors of the document listings. Feed it back verbatim in `starting_after`. `discard_reason_label` is returned in **Spanish**, the language of the product's own interface. If you build a panel in another language, key your own copy on `discard_reason` — that value is the stable, closed identifier. ### Filters [#filters] | Filter | Values | Notes | | ------------------------------------ | ------------------------------------------------------------------------------------ | --------------------------- | | `provider` | `stripe`, `gocardless`, `monei`, `slack`, `teams`, `a3`, `norma43`, `norma19`, `ubl` | Closed set | | `status` | `success`, `skipped`, `failure` | Closed set | | `event_type` | free-form text, exact match, up to 100 characters | **Not** an enum — see below | | `discard_reason` | one of the twenty reasons of the catalogue | Closed set | | `is_parked` | `true` / `false` | See the note below | | `created_at[gte]`, `created_at[lte]` | ISO 8601 | Inclusive window | **`discard_reason` is the closed axis; `event_type` is not an enum.** The `event_type` column deliberately mixes two conventions: branches instrumented later store the raw provider type (`charge.refunded`), while pre-existing ones keep their own semantic value (`autoinvoice.*`). Match it exactly when you know what you are after, but never model it as a closed set — you would be modelling something the column does not guarantee. **`is_parked=false` is not the same as omitting the parameter.** The first one excludes parked events; the second one excludes nothing. A value outside its catalogue returns **422**, and an unknown query parameter returns **400 `parameter_unknown`** instead of being silently ignored — a filter that gets dropped in silence hands you a page you believe is narrowed when it is not. ## The catalogue of discard reasons [#reasons] Twenty typed reasons, one per discard branch of the gateway webhook pipeline. Each one declares two business decisions that are **not** decorative flags: * **Actionable** — can the account owner do something about it? Only actionable reasons notify. Telling someone about a discard they cannot resolve teaches them to ignore the inbox, and that is how the notification that mattered gets missed. * **Parked** — could reprocessing the same content produce a different outcome? Only parked events keep their content encrypted and accept a replay. The rule behind the parked column: an event is parked when the discard was caused by an **external state you can change** (a toggle that is off, a connected account that came unlinked, a currency with no exchange rate yet). It is not parked when the cause is the **content of the event itself** (malformed, duplicate, uncovered type, zero amount, cycle already invoiced) — reprocessing it would take exactly the same branch and only write a second row. Hence the invariant: **every parked reason is actionable**, and six of the nine actionable ones are parked. | Reason | What causes it | Actionable | Parked | What to do | | ------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------- | | `event_not_normalizable` | Malformed event, or of a type that cannot be interpreted | No | No | Nothing — you cannot fix the provider's payload | | `duplicate_redelivery` | The event was already processed; its effect exists | No | No | Nothing — reprocessing would be a no-op by deduplication | | `connected_account_missing` | The webhook is misconfigured at the gateway: the event does not say which account it belongs to | **Yes** | No | Check at the gateway that the webhook is sent from the account you linked in Factuarea | | `connected_account_unknown` | The account exists at the gateway but is not linked in Factuarea | **Yes** | **Yes** | Re-link that gateway account and replay the event | | `spontaneous_payment_missing_id` | The charge carries no id, so there is no idempotency key | No | No | Nothing — replaying would either duplicate or fail again | | `autoinvoicing_disabled` | Auto-invoicing is off for that integration | **Yes** | **Yes** | Either turn auto-invoicing on and replay, **or** create the invoice by hand — never both | | `unsupported_currency` | The European Central Bank rate for the day is not available yet | **Yes** | **Yes** | Replay the event later, once the official rate of the day is published | | `refund_without_items` | The refund carries no individual refunds to correct | No | No | Nothing — there is nothing to issue | | `refund_autoinvoicing_disabled` | Automatic corrective invoices are off for that integration | **Yes** | **Yes** | Either turn automatic correctives on and replay, **or** issue the corrective by hand — never both | | `subscription_missing_invoice_id` | The billed cycle has no invoice id | **Yes** | No | Create the invoice for this cycle by hand; replaying would give the same result | | `subscription_proration_review` | A standalone proration was charged and needs a human decision | **Yes** | No | Check the proration amount at the gateway and issue the invoice by hand | | `subscription_not_a_cycle` | The gateway invoice does not correspond to a billable subscription cycle | No | No | Nothing — the discard is correct | | `subscription_trial_skipped` | Zero or negative amount (trial or credit): no taxable base | No | No | Nothing — there is nothing to invoice | | `subscription_autoinvoicing_disabled` | Subscription auto-invoicing is off | **Yes** | **Yes** | Turn subscription auto-invoicing on and replay the event | | `subscription_already_invoiced` | The cycle already has its invoice | No | No | Nothing — reprocessing would be a no-op by idempotency | | `payout_missing_id` | The payout carries no identifier | No | No | Nothing — it can be neither reconciled nor safely replayed | | `payout_connected_account_missing` | The payout's connected account is not linked | **Yes** | **Yes** | Link the connected account and replay the event | | `payment_failed` | The charge failed at the gateway | No | No | Nothing — there is nothing to issue or retry | | `event_type_not_covered` | Event type outside the product's scope | No | No | Nothing — replaying would do nothing again | | `checkout_lines_retrieve_failed` | Degradation, not a discard: the invoice **was** issued, with a single line | No | No | Nothing to replay; review the invoice's lines if the breakdown matters to you | A reason with nothing to do says so explicitly. Eleven of the twenty are informational, and the contract does not invent an instruction for them: the detail endpoint returns `recommended_action: null` rather than a sentence manufactured to fill the field. <Callout type="warn"> **"Either … or" means either, not both.** Two reasons offer you two ways out — turn the toggle on and replay, or issue the document by hand. They are **mutually exclusive**. Replay idempotency keys on the identity of the charge and only recognises documents issued through that same automatic path, so an invoice you created by hand does **not** stop it. Doing both leaves the same charge with **two invoices**, each numbered in its series and registered in VeriFactu — fiscal damage that can only be undone with a corrective invoice. </Callout> ## Notifications: only what you can fix [#notifications] An actionable discard notifies the account's administrators. An informational one never does. The notification is throttled: if an **unread** notice already exists for the same company, gateway and reason within the last 24 hours, no second one is created — a misconfigured webhook fires hundreds of identical events. The condition is *unread* on purpose: once you have read it and discards keep arriving, the next one **does** notify. That is not noise, it means the incident is still live. ## Parking and the 30-day window [#retention] When a reason is parkable, Factuarea stores the raw event **encrypted at rest**, so that it can be reprocessed later. That content is **never returned** by the API — not in the listing, not in the detail. It holds personal data of your end customers and payment details, and it exists for exactly one purpose: making the replay possible. The content is **purged 30 days after the event was parked**. The row survives: its reason, its status, its date and its `is_parked` flag stay in your inbox indefinitely, because the record that a charge did not produce an invoice is history you may need long after the content expired. <Callout type="info"> **An event that is still `is_parked: true` but no longer `is_replayable` means exactly one thing: the retention window elapsed.** The flag is derived from whether the content is still there, so it flips on its own the day the purge runs. A replay attempted after that returns 422 with the subcode `integration_event_payload_purged`. </Callout> ## The detail: what to do next [#detail] The detail endpoint returns everything the listing does, plus `recommended_action`: one imperative sentence with the next step for that specific reason, or `null` when the reason is informational. ```bash curl https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` The sentence deliberately tells reproducible reasons ("… and replay the event") apart from the ones that are not ("… issue it by hand"), so it never points you at an operation that would answer 422. An event of another company and an event that does not exist return the **same** 404 `resource_not_found`. The endpoint never reveals whether an id exists elsewhere. ## Replaying a parked event [#replay] Reprocess a gateway event that was parked, once the cause that prevented it from producing its effect is gone — you turned automatic invoicing back on, you re-linked the connected account, the official exchange rate of the day became available. <Callout type="warn"> **This action can have real fiscal consequences.** If the cause of the discard is already resolved, the replay **can issue a real invoice**, with its series number and its registration in VeriFactu. It is not an innocuous retry: confirm with the account owner before calling it. That is why it takes its own write scope, `integration_events:write`, instead of the read scope of the inbox — a read-only credential must never be able to invoice. </Callout> ```bash curl -X POST https://api.factuarea.com/v1/integrations/events/0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f/replay \ -H "Authorization: Bearer $FACTUAREA_API_KEY" ``` Four properties of this operation matter more than its signature: * **It does not duplicate invoices.** The replay goes through the very same idempotency check as the original attempt, so if that charge already produced an invoice, the job stops on its own and creates nothing. * **It is asynchronous.** `202` means accepted and queued, **not** completed. The body returns the event as it stands *now* — its `is_replayable` is still `true` — not the outcome of the retry. The outcome shows up as a **new** event in the inbox, so poll `GET /v1/integrations/events` to see how it ended. * **If the cause is still present, the event is discarded again** and recorded once more. That is correct, and it is observable. * **It takes no input.** Any query parameter or body key returns **400 `parameter_unknown`** instead of being ignored. Sending one means you believe you are configuring something about the retry — a mode, a series, a date — that this operation does not support, and silently accepting it would confirm that false expectation about an action that can issue an invoice. An empty body or no body at all is the normal case. ### When a replay is refused [#replay-422] `is_replayable: true` is the contract: when it is true, the replay does **not** answer 422. It is the conjunction of three conditions — the event is parked, it still holds its content, and its reason admits reprocessing — evaluated in that same order by the very handler that guards the replay. That is what lets you offer a retry button without guessing. The three refusals all return **422 `business_rule_violation`** and tell you which one it is through the `subcode`: | `subcode` | What it means | Is there a way forward? | | ----------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------ | | `integration_event_not_parked` | The event was never parked — it either succeeded, or its reason does not keep the content | No, and there never will be | | `integration_event_payload_purged` | It was parked, but its content was deleted when the 30-day window elapsed | No — handle it by hand | | `integration_event_reason_not_replayable` | It is parked and still holds its content, but its reason would take exactly the same branch again | No — follow the recommended action instead | ## Where this fits [#related] * [Stripe auto-invoicing](/payments/stripe-autoinvoicing) — the flow that produces most of the events you will find here, including the [subscription cycles](/payments/stripe-autoinvoicing#subscriptions) whose toggle is behind `subscription_autoinvoicing_disabled`. * [Payouts and bank reconciliation](/payments/payouts-reconciliation) — the payout ingestion behind `payout_missing_id` and `payout_connected_account_missing`. * [Test mode](/guides/test-mode) — validate your handling of the inbox with a `fact_test_` key before you wire a replay button to a production credential. * [Error codes](/guides/errors) — the envelope of the 400, 404 and 422 responses quoted above. --- # Reconciling with system metadata (/payments/metadata-reconciliation) Every document in Factuarea carries a free-form `metadata` object you can write whatever you need into. On invoices that Factuarea issues **automatically from a Stripe subscription cycle**, the platform also writes a handful of **system keys** that tie the invoice back to the charge it came from: which Stripe invoice, which subscription, which billing period. Those keys are what makes reconciliation possible without keeping your own mapping table. They have been written for a while; this page is where they get documented. <Callout type="info"> **Scope: subscription cycles.** These keys are written by the flow that auto-issues an invoice for a **billed subscription cycle** (see [subscription cycles](/payments/stripe-autoinvoicing#subscriptions)). One-shot charges auto-invoiced from `charge.succeeded` do **not** carry them today — for those, correlate through the [auto-invoiced charges listing](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list), which exposes the charge-side identifiers. </Callout> ## The system keys [#keys] | Key | What it identifies | Format | Presence | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `stripe_invoice_id` | The Stripe invoice of the billed cycle | Stripe id, `in_…` | **Always** | | `billing_reason` | Why Stripe billed that invoice | Stripe's raw `billing_reason` — in practice `subscription_create` (first cycle) or `subscription_cycle` (each renewal), the only two that are auto-invoiced | **Always** | | `stripe_subscription_id` | The subscription the cycle belongs to | Stripe id, `sub_…` | Optional — omitted when Stripe sends no subscription id | | `period_start` | First day of the billed period | `YYYY-MM-DD`, **UTC** | Optional — omitted when the period timestamp is absent | | `period_end` | End of the billed period, verbatim from Stripe's `period_end` — the **exclusive** boundary, so for a monthly cycle it is the first day of the next period, not the last day of this one | `YYYY-MM-DD`, **UTC** | Optional — omitted when the period timestamp is absent | Optional keys are **not materialised as null or empty**: when the value does not apply, the key is not written at all. That is deliberate — a key present with an empty value would look like a correlation that exists but is blank, and any code reading it would have to distinguish "no subscription" from "subscription unknown". Check for the key's presence, not for its value. <Callout type="warn"> **These are system keys. Do not write them by hand.** They are the correlation between a Factuarea invoice and a Stripe object, and the reconciliation recipes below trust them. Writing `stripe_invoice_id` yourself on an unrelated invoice makes that invoice show up in a reconciliation it does not belong to, and nothing will flag it — `metadata` is free-form by design. Use your own keys (`erp_ref`, `project_code`, …) for your own correlations. </Callout> The keys are readable wherever the invoice is: `metadata` is part of the invoice resource, and it comes back as a JSON object (`{}` when empty). ## Filtering by metadata [#filter] Eight v1 listings accept a `metadata` filter: | Resource | Endpoint | | ------------------ | ------------------------------------------------------------------------------------------------------- | | Invoices | [`GET /v1/invoices`](/api-reference/invoices/public-api.v1.invoices.list) | | Quotes | [`GET /v1/quotes`](/api-reference/quotes/public-api.v1.quotes.list) | | Pro-forma invoices | [`GET /v1/proformas`](/api-reference/proformas/public-api.v1.proformas.list) | | Delivery notes | [`GET /v1/delivery_notes`](/api-reference/delivery-notes/public-api.v1.delivery_notes.list) | | Purchase invoices | [`GET /v1/purchase_invoices`](/api-reference/purchase-invoices/public-api.v1.purchase_invoices.list) | | Recurring invoices | [`GET /v1/recurring_invoices`](/api-reference/recurring-invoices/public-api.v1.recurring_invoices.list) | | Products | [`GET /v1/products`](/api-reference/products/public-api.v1.products.list) | | Suppliers | [`GET /v1/suppliers`](/api-reference/suppliers/public-api.v1.suppliers.list) | The syntax is `deepObject`: `metadata[key]=value`, one query parameter per pair. * **Pairs combine with AND.** Two pairs return the documents that match both. * **Exact match** on the value; there is no partial or prefix matching. * **Up to 50 pairs** per request; more returns `parameter_invalid_range`. * **Keys** must match `[A-Za-z0-9_.-]` and be 1 to 64 characters long; anything else returns `parameter_invalid_enum`. * The filter sits **outside** the `{operator, value}` contract of the column filters, so there is no `metadata[key][eq]` form. `metadata[key]=value` is the whole syntax. <Callout type="info"> **Let curl encode the brackets.** `[` and `]` are glob characters for curl and reserved characters in a URL. Pass the pairs with `-G --data-urlencode`, as in the recipes below, and curl encodes them correctly. Pasting a raw `?metadata[key]=value` into a shell is where "the filter is being ignored" usually comes from. </Callout> ## Recipe: every invoice of one subscription [#recipe-subscription] The reconciliation you need when a customer asks for all the invoices of their plan, or when you close the year for one subscriber: ```bash curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \ --data-urlencode "limit=100" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7c1d-9e8f-1a2b3c4d5e6f", "object": "invoice", "number": "2026/0184", "total": "49.90", "currency": "EUR", "metadata": { "stripe_invoice_id": "in_1QRstuVWXYZabcde", "billing_reason": "subscription_cycle", "stripe_subscription_id": "sub_1QRstuVWXYZabcde", "period_start": "2026-07-01", "period_end": "2026-08-01" } } ], "has_more": false, "next_cursor": null } ``` The listing is cursor-paginated like every other one: keep reading while `has_more` is `true`, feeding `next_cursor` back into `starting_after`. See [pagination](/guides/pagination). ## Recipe: the invoices of one billing period [#recipe-period] Two pairs, combined with AND: the subscription and the first day of the period. This is the query that answers "did July's cycle get invoiced?". ```bash curl -G https://api.factuarea.com/v1/invoices \ -H "Authorization: Bearer $FACTUAREA_API_KEY" \ --data-urlencode "metadata[stripe_subscription_id]=sub_1QRstuVWXYZabcde" \ --data-urlencode "metadata[period_start]=2026-07-01" ``` Because `period_start` and `period_end` are exact dates in **UTC**, filter on the period boundary itself rather than on a range — the value in metadata is the day Stripe reports for the cycle, not a local calendar month. To sweep a whole month of cycles across every subscription, drop the subscription pair and query `metadata[period_start]` alone. <Callout type="warn"> **Filter on `period_start`, not on `period_end`.** `period_end` is Stripe's exclusive upper bound: the July cycle of a monthly subscription carries `period_start: 2026-07-01` and `period_end: 2026-08-01`. Querying `metadata[period_end]=2026-07-31` returns nothing, and the empty result looks exactly like a cycle that was never invoiced. </Callout> An empty `data` array for a period you expected to be invoiced is a real signal, not a filter mistake. That is exactly the case the [integration event inbox](/payments/integration-events-inbox) explains: open it filtered by `provider=stripe` and `status=skipped` and the typed discard reason will tell you whether the cycle was skipped because auto-invoicing was off, the cycle carried no amount, or something else — and whether you can replay it. ## Related [#related] * [Stripe auto-invoicing](/payments/stripe-autoinvoicing) — how the invoices these keys describe get issued in the first place. * [Integration event inbox](/payments/integration-events-inbox) — why a cycle you expected never produced an invoice. * [Tags and custom fields](/guides/tags-and-custom-fields) — writing and querying your **own** metadata keys. --- # MONEI (/payments/monei) <Callout type="warn"> **MONEI is not available yet.** Its v1 endpoints and MCP tools are **not registered**, so calling them today returns `404 route_not_found`. This page documents the **status** of the integration and the surface that will appear when it is released — it is not a usage guide. </Callout> MONEI is a Spanish payment gateway that collects by **card and Bizum**. Money moves at the moment of capture, like a card gateway and unlike SEPA Direct Debit — which is the reason its integration is shaped slightly differently from [GoCardless](/payments/gocardless). ## What "not released" means [#status] The single source of truth is the backend's list of released payment gateways (`integrations.released_providers`), which today contains **Stripe only**. It is configuration, not code, so a gateway is switched on without a code deploy. While MONEI is outside that list: * Its **v1 route block is not registered**. The `/v1/monei-autoinvoicing/*` endpoints do not exist — they are absent from the route registry, from the OpenAPI spec and from the API reference of this site. * Its **MCP tools are filtered out** of the public server. * The gateway shows as **"Próximamente"** (coming soon) in the integrations marketplace of the dashboard, and the connect flow is blocked at the command handler as well. * The gateway-agnostic connected-accounts endpoint filters its results to released gateways, so no MONEI account surfaces through it either. The classes are **asleep, not absent**. The release flips one list. ## No mandates resource, and that is not an omission [#no-mandates] GoCardless collects by SEPA Direct Debit, so a **mandate** — the customer's standing authorisation to pull money from their bank account — is a first-class object with its own life cycle, and it gets its own endpoint and its own MCP tool. **MONEI does not use SEPA Direct Debit.** There is no standing authorisation to model, so there is no `mandates` resource, no mandate states to keep in sync and no mandate webhooks. If you are porting an integration written against GoCardless, that whole branch disappears; there is nothing to map it onto. ## What already exists behind the flag [#built] | Piece | State | | ------------------------------------- | -------------------------------------------------------------------------------------------------- | | Connect flow | Built. MONEI authenticates with an **API key**, not OAuth 2 | | Webhook signature verification | Built | | Event normaliser | Built — maps MONEI payment statuses onto the same internal payment events the Stripe pipeline uses | | Per-gateway connected accounts | Built — list, retrieve, update and disconnect, mirroring the Stripe multi-store model | | Auto-invoiced charges and correctives | Built — same decision rules, same VeriFactu registration as Stripe | | Mandates | **Not applicable** — see above | ### Which statuses invoice, and which deliberately do not [#events] MONEI reports the state of a payment as a status on the payment object, and the normaliser keys on it: | MONEI status | What it produces | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `SUCCEEDED` | Treated as **paid** → the auto-invoicing flow runs | | `REFUNDED`, `PARTIALLY_REFUNDED` | Treated as a **refund** → corrective invoice flow, total or partial | | `FAILED`, `CANCELED` | Recorded as a failed charge; nothing is issued | | `AUTHORIZED` | **Deliberately ignored** — an authorisation without capture is not money collected, and invoicing it would invoice a charge that may never be captured | | Anything else | Recorded as an unknown event | A payment without an identifier is dropped before anything else: with no id there is no canonical identity and no idempotency key, so it could neither be deduplicated nor safely replayed. ## The surface that appears on release [#future-surface] ### v1 endpoints [#future-endpoints] | Endpoint | Scope | | ------------------------------------------------------------- | --------------------------- | | `GET /v1/monei-autoinvoicing/connected-accounts` | `monei_autoinvoicing:read` | | `GET /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:read` | | `PUT /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:write` | | `DELETE /v1/monei-autoinvoicing/connected-accounts/{account}` | `monei_autoinvoicing:write` | | `GET /v1/monei-autoinvoicing/payments` | `monei_autoinvoicing:read` | | `GET /v1/monei-autoinvoicing/correctives` | `monei_autoinvoicing:read` | The charges listing carries an `origin` filter (`subscription` / `oneshot`) for symmetry with the other gateways. If MONEI has no subscription charges for you, `origin=subscription` returns an empty page rather than an error. ### MCP tools [#future-tools] `list_monei_connected_accounts`, `get_monei_connected_account`, `update_monei_connected_account`, `disconnect_monei_connected_account`, `list_monei_autoinvoiced_payments` and `list_monei_autoinvoiced_correctives` — one per endpoint above, with the same scopes. ### Plan requirement [#plan] The MONEI integration is a module of the **Empresario** and **Enterprise** plans, like the Stripe and GoCardless integrations. Being on the right plan will not be enough on its own while the gateway is unreleased — both conditions have to hold. ## Mapping against the Stripe flow [#stripe-parity] The gateway-specific part ends at the normaliser: from there on, every gateway shares the same invoicing pipeline, the same fiscal decisions and the same VeriFactu registration described in [Stripe auto-invoicing](/payments/stripe-autoinvoicing). | Concept | Stripe | MONEI | | ------------------------- | --------------------------------------------------- | ---------------------------------------------------------- | | Authentication | OAuth 2 (Stripe Connect) | API key | | Multi-store | `connected-accounts` per account | Same model, under `monei-autoinvoicing/connected-accounts` | | "Charge succeeded" signal | `charge.succeeded` / `invoice.paid` | Payment status `SUCCEEDED` | | Refunds | `charge.refunded` → corrective invoice | `REFUNDED` / `PARTIALLY_REFUNDED` → corrective invoice | | Uncaptured authorisation | Not invoiced | `AUTHORIZED`, not invoiced | | Mandates | Not applicable | Not applicable | | Subscription cycles | `invoice.paid` with a subscription `billing_reason` | No equivalent branch in the normaliser | | Payout reconciliation | [Supported](/payments/payouts-reconciliation) | Not covered today | ## What works today regardless [#inbox] The [integration event inbox](/payments/integration-events-inbox) is **gateway-agnostic** and is registered unconditionally, so `provider=monei` is a valid filter value there on day one — including for historical events, which is precisely why those rows are not hidden. --- # Payouts & bank reconciliation (/payments/payouts-reconciliation) When you connect Stripe via **Stripe Connect**, Stripe doesn't transfer each charge to your bank one by one — it batches many charges, subtracts its fees, and sends a single **payout** (`po_xxx`) to your account. The line that lands on your bank statement reads `STRIPE PAYOUT 1.234,56 €` and is the **net** of *N* charges minus fees, so it never matches the total of any single invoice. Factuarea closes that loop. It **ingests every payout**, links it to the charges that make it up, and reconciles the bank line against the payout — not against an invoice. When you confirm the match, the payout, the bank transaction and all the underlying charges are marked reconciled in one atomic step. Payouts are **read-only on the public API**: you can list and inspect them and their reconciliation state, but the reconciliation itself happens in the dashboard against your imported Norma 43 statement. Two v1 endpoints expose them: * [List Stripe payouts](/api-reference/stripe/public-api.v1.payouts.list) (`payouts:read`). * [Retrieve a Stripe payout](/api-reference/stripe/public-api.v1.payouts.show) (`payouts:read`). ## Ingesting a payout [#ingestion] Every time Stripe completes a payout it sends a **`payout.paid`** webhook to your Connect endpoint. Factuarea reacts to it: 1. It records the payout — `connected_account_id` (`acct_xxx`), `stripe_payout_id` (`po_xxx`), the **net**, **fees** and **gross** amounts, the currency and the expected **arrival date** — with `status: ingested`. 2. It reads the payout's **balance transactions** on your behalf (a read-only, paginated call to Stripe) to discover **which charges** the payout groups and the total fees. That breakdown is stored as the informative `composition`. Ingestion is **idempotent on two levels**: by the Stripe `event.id` (a redelivered `payout.paid` is processed at most once) and by the `stripe_payout_id` (two different events for the same `po_xxx` never create a duplicate row — the uniqueness is guaranteed in the database, even under concurrent webhooks). <Callout type="info"> If the breakdown can't be read (a transient Stripe error after retries), the payout is **not** half-ingested: the whole step is retried, and the `event.id` is only marked processed once ingestion fully succeeds. There is never a payout row without its amounts. </Callout> <Callout type="warn"> To ingest payouts you must enable the **`payout.paid`** event on your Connect webhook endpoint in the Stripe Dashboard. As always, validate the flow with a `fact_test_` key first — in the [sandbox](/guides/test-mode) ingestion runs against an isolated company with all external effects switched off. </Callout> ## Linking charges to the payout [#linking] The balance transactions tell Factuarea which charges compose the payout. Each component carries its Stripe `payment_intent` (`pi_xxx`) — the **same identifier** Factuarea stamped on the `Payment` it recorded when the charge was auto-invoiced (see [Stripe auto-invoicing](/payments/stripe-autoinvoicing)). Using that identifier, Factuarea finds the matching `Payment` records of your company and stamps the payout id on each one. So a payout's charges are linked to the cobros that produced them — the relationship that later lets reconciliation cascade down to every charge. Linking is **best-effort and idempotent**: re-linking the same payout is not an error, and a component with no corresponding `Payment` (a charge collected before auto-invoicing existed, or by another tool) is recorded in the [integration event inbox](/payments/integration-events-inbox) without blocking the rest. A payout that arrives before you link its connected account is parked there as `payout_connected_account_missing`: link the account and replay the event, and the payout is ingested. The payout is ingested regardless — reconciliation matches on the **net amount**, it never requires a complete charge breakdown. ## Reconciling against the bank statement [#reconciliation] Reconciliation runs over your imported **Norma 43** bank statement, in the dashboard. When you upload a statement, Factuarea proposes matches for each credit line. **Before** trying to match a credit against a pending invoice, it checks whether the line is a **payout**: | Signal | Rule | | ------------------ | ---------------------------------------------------------------------------------------------------------------- | | **Amount** | The bank credit equals the payout's **net** amount (within a small rounding tolerance). This is the hard signal. | | **Arrival window** | The bank value date falls within **±3 days** of the payout's `arrival_date` (banks settle with a small lag). | | **Description** | A `STRIPE` mention **adds confidence** but is never required — the wording varies between banks. | The outcome depends on how many `ingested` payouts fit: * **Exactly one** candidate → an **automatic** payout match. * **More than one** → a **suggestion** with the candidates, for you to pick. * **None** → the line continues to the ordinary **per-invoice** matching (a credit that isn't a payout should still match an invoice). A transaction matched against a payout is **excluded** from per-invoice matching, and vice-versa, so the same bank line is never reconciled twice. <Callout type="info"> Matching is **per currency**. The payout is ingested in its real currency and only matches bank lines in the **same** currency — there is no conversion (this is accounting reconciliation, not a fiscal operation, so it never issues or alters an invoice). </Callout> ## Confirming the match [#confirm] When you confirm a payout match in the dashboard, Factuarea performs **one atomic transaction**: 1. The bank transaction is marked **reconciled** (with match type `payout`). 2. The payout transitions to **`reconciled`** (a terminal state) and records the reference of the bank transaction in `bank_transaction_ref`. 3. Every `Payment` linked to the payout is stamped with its `reconciled_at` and the bank transaction reference. Guards protect every step: the bank transaction must still be `pending`, the payout must still be `ingested`, and the amounts must agree. Confirming a payout that is **already reconciled**, or a transaction that is **already matched**, is rejected with no partial state left behind. The whole operation is tenant-scoped: a payout or transaction of another company is never visible nor reconcilable. ## Inspecting payouts on the API [#api] List your company's payouts with cursor pagination, filtered by reconciliation `status` and by arrival-date window: ```bash curl "https://api.factuarea.com/v1/payouts?status=ingested&arrival_date[gte]=2026-01-01&limit=25" \ -H "Authorization: Bearer fact_test_…" ``` ```json { "data": [ { "id": "0192f3a4-7b2c-7e10-9c1a-1f2e3d4c5b6a", "object": "stripe_payout", "connected_account_id": "acct_1QabcDEF2ghIJklm", "stripe_payout_id": "po_1QabcDEF2ghIJklm", "amount_net": "1234.56", "fee_total": "37.04", "amount_gross": "1271.60", "currency": "EUR", "arrival_date": "2026-01-08", "status": "ingested", "reconciled_at": null, "bank_transaction_ref": null, "composition": { "components": [ { "payment_intent": "pi_3QabcDEF2ghIJklm", "charge_id": "ch_3QabcDEF2ghIJklm", "amount": 121.00, "fee": 3.50 } ], "fee_total": 37.04 } } ], "has_more": false, "next_cursor": null } ``` Every identifier is **opaque**: * `id` is the payout **UUID v7** — the public identity of the resource. * `connected_account_id` (`acct_xxx`) and `stripe_payout_id` (`po_xxx`) are **external Stripe ids**, not foreign keys to other Factuarea resources. * `bank_transaction_ref` is the UUID (v7) of the reconciled bank statement transaction — `null` while the payout is still `ingested`. * `composition` references **opaque Stripe ids** (`payment_intent` = `pi_xxx`, `charge_id` = `ch_xxx`), not internal payment UUIDs. It may be empty when the breakdown couldn't be read. A payout's `status` is `ingested` until it is reconciled against a bank line, then `reconciled` (terminal). Retrieve a single payout by its `id`: ```bash curl "https://api.factuarea.com/v1/payouts/0192f3a4-7b2c-7e10-9c1a-1f2e3d4c5b6a" \ -H "Authorization: Bearer fact_test_…" ``` It returns `404` if the payout doesn't exist or belongs to another company. ## The outbound event [#event] When a payout is reconciled, Factuarea emits the **`payout.reconciled`** event. Its payload carries the full payout snapshot (`object`) plus the net amount, currency and the bank transaction reference, so a webhook receiver can close its own books the moment the money is confirmed in the bank. Subscribe to it like any other event — see [Webhooks](/guides/webhooks) and [Events](/guides/events). There is no `payouts:write` scope: payouts are observed, never mutated, through the API. The only state change — reconciliation — is driven from the dashboard against your Norma 43 statement, and the event is what notifies your integration. --- # Stripe auto-invoicing (/payments/stripe-autoinvoicing) When you connect Stripe via **Stripe Connect**, Factuarea can **auto-issue an invoice for every successful charge**: the charge is turned into an invoice with `status: sent`, registered for VeriFactu, and a `Payment` is recorded against it. The flow is idempotent end-to-end, so a redelivered webhook never produces a duplicate invoice. Two flows feed it: * **Flow A** — a charge that pays an existing Factuarea invoice (a Checkout Session Factuarea created from a payment link). The invoice already exists; the charge marks it paid. * **Flow B** — a spontaneous charge with no prior invoice (a Payment Link the merchant created in their own Stripe Dashboard, or any other Connect charge). Factuarea creates the invoice from the charge. The configuration is read and written through the company-wide v1 endpoints: * [Retrieve the auto-invoicing config](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show) (`stripe_autoinvoicing:read`). * [Update the auto-invoicing config](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.update) (`stripe_autoinvoicing:write`). * [List auto-invoiced charges](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list) (`stripe_autoinvoicing:read`). * [List auto-invoiced correctives](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list) (`stripe_autoinvoicing:read`). If you run several **stores** under separate Stripe accounts, each account has its own series and its own configuration — see [Multiple stores](#multi-store). Auto-invoicing is gated by the **Stripe integration** module of your plan and is **off by default** — enable it explicitly with `enabled: true`. <Callout type="info"> **What the Stripe API exposes.** Company-wide auto-invoicing **configuration** (a convenience inherited from the single-store model — the real source of truth is the per-account config, see [Multiple stores](#multi-store)), **connected accounts**, **auto-invoiced charges**, **correctives**, and **[payouts](/payments/payouts-reconciliation)** for bank reconciliation. </Callout> ## Multiple stores [#multi-store] A business can run several "stores" or lines (a physical shop + an online course) under **different Stripe accounts** (Stripe Connect) and want **independent invoice numbering for each one** (`TIENDA-2026-…`, `CURSOS-2026-…`). Factuarea models each Stripe account you connect as a **connected account**: every Account Link you complete **adds** an account — it never overwrites the previous one — and charges on each account are routed to **that account's series and configuration**. Each connected account carries: * a **series** (`series_id`) used for the invoices auto-created from its charges — `null` means the **company default invoice series** is used; * its **own auto-invoicing configuration** (`autoinvoicing_enabled`, `simplified_threshold_cents`, `require_nif`, `refunds_enabled`, `subscription_autoinvoicing_enabled`) — every fiscal rule on this page applies per account. When a webhook arrives, Factuarea resolves the Stripe account (`acct_xxx`) to its connected account and issues the invoice **in that account's series**, with that account's fiscal policy — so two stores produce invoices in two separate, correct numbering series. ### New accounts start safe [#multi-store-defaults] A newly connected account does **not** inherit another account's configuration: it starts with the same safe defaults as a fresh setup — auto-invoicing **off**, threshold **400 €**, "require NIF" off, refunds on, subscriptions off — and no series (it falls back to the company default until you assign one). Configure it explicitly before it issues anything. ### Per-account v1 endpoints [#multi-store-api] Manage accounts under the `connected-accounts` resource (same `stripe_autoinvoicing:read|write` scopes; identity is the account `id`, a UUID v7): * [List connected accounts](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.list) (`stripe_autoinvoicing:read`). * [Retrieve a connected account](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.show) (`stripe_autoinvoicing:read`). * [Update a connected account](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.update) — name, `series_id` (send `null` to clear it), and the per-account configuration (`stripe_autoinvoicing:write`). * [Disconnect a connected account](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.accounts.disconnect) (`stripe_autoinvoicing:write`). ```bash # Assign the CURSOS series and enable auto-invoicing on one store curl -X PUT https://api.factuarea.com/v1/connected-accounts/0192f3a4-… \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "series_id": "0192aaaa-…", "autoinvoicing_enabled": true }' ``` Disconnecting an account keeps its already-issued invoices and history; later webhooks for it are recorded **without** processing. Referencing the `id` of an account that belongs to another company returns `404` (`connected_account_not_found`) — multi-tenant isolation never leaks existence. ### The company-wide endpoint while you have one store [#multi-store-legacy] The company-wide [config endpoints](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.config.show) above stay valid **while you have exactly one connected account**: the `GET` returns that single account's effective config, and the `PUT` **proxies** to it (it writes the account's configuration, never a duplicate company-level copy). As soon as you connect a **second** account, the company-wide config can no longer answer "which account?". Both the `GET` and the `PUT` then return `422` (`per_account_config_required`, message in Spanish) pointing you to the per-account endpoints — Factuarea **never writes to two places**, so there is no divergence between a company-level config and the accounts. <Callout type="info"> **One source of truth.** The per-account configuration is the only place settings live. The company-wide endpoint is a convenience that proxies to the single account; it never holds a separate copy, so reading and writing always agree. With two or more accounts, use `connected-accounts/{account}` directly. </Callout> ## Ordinary or simplified invoice [#decision] A Spanish invoice needs the recipient's **NIF** to be issued as an ordinary invoice (F1). A B2C charge without a NIF is exactly the case the law settles with a **simplified invoice (F2)**. Factuarea decides which one to issue from the data the charge carries plus your fiscal policy: | Situation | Invoice issued | | ----------------------------------------------------------------------------------------- | --------------------------------------------- | | A **valid NIF** is captured in Checkout, or the resolved client already has a NIF on file | **Ordinary (F1)** | | **No NIF**, charge total **at or below** the threshold, and "require NIF" is off | **Simplified (F2)** | | **No NIF** and (total **above** the threshold **or** "require NIF" is on) | **Manual review** — no invoice is auto-issued | A captured NIF is validated against the Spanish format (NIF/NIE/CIF). A NIF with an invalid format counts as **no NIF**, so an F1 is never issued with junk data. <Callout type="info"> Charges routed to manual review are **not lost**: they are recorded in the integration log so you can issue the invoice by hand. Auto-invoicing keeps going for the rest — a charge in review never fails the webhook. They are all listed in the [integration event inbox](/payments/integration-events-inbox), which is where you see what happened to each one. </Callout> The absolute legal ceiling for a simplified invoice is **3,000 €**, enforced by the invoicing domain itself: a charge without a NIF above 3,000 € always goes to manual review, whatever the threshold is set to. ## Capturing the NIF in Checkout [#nif-capture] So a client who *does* have a NIF can provide it, the Checkout Sessions Factuarea creates (Flow A) enable Stripe's **tax-ID collection**. The client can enter their `es_cif`/`eu_vat` at payment time, and that NIF drives the F1 path. NIFs are also read from any incoming `checkout.session.completed`: * from `customer_details.tax_ids` (the standard Stripe field), and * from the **custom fields** of Payment Links the merchant builds in their own Stripe Dashboard — Factuarea looks for a field whose key looks like a tax ID (`nif`, `dni`, `cif`, `vat`, `tax`). A charge that arrives only as `payment_intent.succeeded` (no Checkout) carries no captured NIF, but it can **still** be an F1 if the client is resolved by email and already has a NIF on file. ## The threshold [#threshold] `simplified_threshold_cents` is the amount **in cents** at or below which a charge without a NIF is auto-issued as a simplified invoice. It defaults to **40000 (400 €)** and accepts any value in the range **\[0, 300000]** (0–3,000 €). A conservative default of 400 € is deliberate: the 3,000 € ceiling is only legal in specific rated sectors, and Factuarea does not know your sector — raise the threshold only if your activity allows it. ```bash curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "enabled": true, "simplified_threshold_cents": 100000, "require_nif": false }' ``` Both fiscal fields are **optional**: omit `simplified_threshold_cents` or `require_nif` and their current values are kept. A threshold outside the range returns `422` (`validation_error`). ## Requiring a NIF [#require-nif] `require_nif` (default `false`) has **two coherent effects**: * In the Checkout Sessions Factuarea creates, the tax ID is marked **required** (`if_supported`), so the client is prompted for it. * In the decision above, it **vetoes the F2**: a charge without a NIF goes to manual review instead of becoming a simplified invoice. Turn it on when your company never wants automatic simplified invoices — every charge then either has a NIF (F1) or waits for you in manual review. <Callout type="warn"> Auto-issued invoices are fiscally real and **irreversible** (the VeriFactu Alta record is created). Validate your fiscal policy with a `fact_test_` key first: in the [sandbox](/guides/test-mode) the VeriFactu record is created locally and never transmitted to the AEAT, so you can exercise the F1/F2/review decision safely before going live. </Callout> ## The outbound event [#event] Every auto-created invoice — F1 or F2 — emits the [`invoice.auto_created`](/api-reference/events/public-api.v1.events.list) event and a `payment.received` event. For a simplified invoice the payload carries `client_id: null` (no recipient), so a webhook receiver can tell F1 and F2 apart. ## Real VAT breakdown with Stripe Tax [#vat-breakdown] If you use **Stripe Tax**, every charge already carries the real tax breakdown per line — the rate, the taxable base, and (where it applies) the reason a line is exempt or reverse-charged. Factuarea **mirrors that breakdown** onto the invoice instead of flattening everything to a single default rate. The Checkout webhook doesn't include the line items, so Factuarea makes a second, read-only API call on your behalf to retrieve them with their taxes, then maps each line: | Stripe Tax data | Invoice line | | ----------------------------------------------------------------------- | --------------------------------------------------------------- | | `rate.percentage` | the line's VAT rate (`vat_rate`), used as-is — never recomputed | | `taxable_amount` | the line's taxable base (unit price = base ÷ quantity) | | `taxability_reason` `zero_rated` / `product_exempt` / `customer_exempt` | **exempt** line at 0 % | | `taxability_reason` `reverse_charge` | **reverse charge (ISP)** line at 0 % | So a charge with mixed VAT (e.g. 21 % consulting + 10 % a book) becomes an invoice with **two real lines**, each at its own rate, and the multi-rate breakdown carries through to the VeriFactu record. <Callout type="info"> The VAT is **never recalculated** — Stripe already computed it, and recomputing would introduce cent-level drift. Factuarea takes the rate and taxable base straight from Stripe Tax. </Callout> **When there's no Stripe Tax** (you haven't enabled it in your Stripe account), nothing changes: every line falls back to your company's **default VAT rate**, exactly like before. A company with no default rate configured falls back to **0 %** — never a phantom 21 %. ## Real line items [#line-items] When a charge carries several line items (several products or concepts), they appear as **real, separate lines** on the invoice — each with its own description, quantity and price — instead of being collapsed into one. The lines are **free lines** (not linked to your product catalogue). This is **orthogonal to the invoice type**: the F1/F2 decision above chooses the *type*, the line mapping chooses the *lines* — both an ordinary and a simplified invoice get the same real lines. A charge that arrives only as `payment_intent.succeeded` (no Checkout Session, so no retrievable line items), or a charge whose line retrieval fails after retries, **still produces an invoice**: it falls back to a single line at the default VAT rate. The invoice is never lost over a non-essential detail. Before issuing, Factuarea **validates the total**: the total derived from the mirrored lines (sum of subtotal + VAT per line, in EUR) must match the amount actually charged, within a small rounding tolerance (±1 cent per line, minimum ±0.05 €). If it doesn't, the charge is routed to **manual review** (`total_mismatch`) instead of issuing an invoice whose total diverges from the real charge — the webhook still succeeds. ## Charges in another currency [#currency] A charge in a currency other than EUR is no longer skipped. Factuarea **converts it to EUR** using the **European Central Bank (ECB) reference rate** for the payment date and issues the invoice **in euros** — base, VAT and total, and the VeriFactu/AEAT record, all in EUR (Art. 12.1 RD 1619/2012: the VAT amount must be stated in euros). * A **EUR** charge is passed through untouched. * A **non-EUR** charge with an available rate is converted; the trace of the conversion — original amount and currency, ECB rate, and rate date — is written to the invoice notes and to the integration log for fiscal auditing. * A charge in a currency with **no ECB rate** available is **not** auto-invoiced: it goes to manual review and the webhook still succeeds. Factuarea never invents a rate. The entry lands in the [inbox](/payments/integration-events-inbox#reasons) as `unsupported_currency`, **parked**: once the official rate for that date is published, replaying the event issues the invoice. The ECB rates are cached daily (no extra database table), so several non-EUR charges on the same day share a single rate lookup. <Callout type="info"> Issuing the invoice **in the original currency** (option B) is intentionally out of scope — Factuarea always converts to EUR (option A). </Callout> ## Refunds and corrective invoices [#refunds] An issued invoice is fiscally **irreversible** — it is never deleted or voided once paid. The only legal way to undo it is a **corrective invoice (rectificativa)**. So when Stripe refunds a charge that Factuarea invoiced, Factuarea closes the fiscal loop for you: the `charge.refunded` webhook generates a **linked corrective invoice automatically** (with its own VeriFactu R record), no manual rectification needed. This is controlled by `refunds_enabled` (default `true`). It only acts while auto-invoicing is enabled — the gate is `enabled && refunds_enabled`. A company that never turned auto-invoicing on sees no change. | Refund | Corrective issued | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **Full** (`refunded: true`) | A `total` corrective that mirrors the whole original invoice as negative lines | | **Partial** (`amount_refunded < amount`) | A `partial` corrective with a single negative line for the refunded amount, at the original line's tax rate | The corrective is always issued **by differences** (AEAT `TipoRectificativa: I`), because a refund is a credit with negative amounts. It carries the `devolucion` correction reason; if the original is a simplified invoice (F2) the corrective is issued as **R5** automatically. The original invoice is located by two paths — by the `factuarea_invoice` metadata on the charge (Flow A) or by the deterministic UUID derived from the `payment_intent` (Flow B) — so refunds of charges issued **before** this feature existed are corrected too. <Callout type="info"> **Idempotency is per individual refund.** Stripe re-emits `charge.refunded` with the *cumulative* `amount_refunded`, but Factuarea keys on each individual refund id (`re_xxx`): each one produces **at most one** corrective. A redelivered event, or a second partial refund, never double-credits. A refund whose original invoice can't be located, isn't in a correctable state (`sent`/`paid`), or is already corrected is recorded in the [inbox](/payments/integration-events-inbox) for manual review — it never fails the webhook. </Callout> Each automatic corrective emits the [`invoice.corrective_auto_created`](/api-reference/events/public-api.v1.events.list) event (carrying the corrective, the original invoice, the originating `refund_id` and the `provider`), and you can list them via [List auto-invoiced correctives](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.correctives.list). <Callout type="warn"> To receive refunds you must enable the **`charge.refunded`** event on your Connect webhook endpoint in the Stripe Dashboard. As with auto-invoicing, validate the flow with a `fact_test_` key first: in the [sandbox](/guides/test-mode) the VeriFactu R record is created locally and never transmitted to the AEAT. </Callout> <Callout type="warn"> **Opting out parks the refunds, it does not discard them.** With `refunds_enabled: false`, each refund is recorded in the [inbox](/payments/integration-events-inbox#reasons) as `refund_autoinvoicing_disabled` and kept, encrypted, for **30 days**. Within that window pick **one** of the two exits, never both: turn `refunds_enabled` back on and [replay](/payments/integration-events-inbox#replay) the event, **or** issue the corrective by hand. Doing both leaves the refund with **two correctives**, each numbered in its series and filed with VeriFactu — the replay's idempotency only recognises correctives issued through that same automatic path, so one you wrote by hand does not stop it. Past the 30 days the content is purged and issuing it by hand is the only route left. </Callout> ## Subscription cycles [#subscriptions] If you charge with **Stripe Billing** on your connected account — recurring monthly or yearly subscriptions — Factuarea can **auto-issue an invoice for each billed cycle**. Every renewal Stripe collects produces its own VeriFactu-compliant invoice, mirroring the real breakdown (lines, period, taxes) Stripe already computed, exactly like one-shot charges. This is a **separate toggle**, `subscription_autoinvoicing_enabled` (default `false`), on top of the general `enabled` flag. **Both must be on** for a cycle to be invoiced — turning on subscriptions alone does nothing while auto-invoicing is globally off. ```bash curl -X PUT https://api.factuarea.com/v1/stripe-autoinvoicing/config \ -H "Authorization: Bearer fact_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 0192f3a4-…" \ -d '{ "enabled": true, "subscription_autoinvoicing_enabled": true }' ``` ### Which cycles are invoiced [#subscription-cycles] Factuarea invoices the **standard billing cycle** and routes everything else to review or ignores it, keyed on Stripe's `billing_reason`: | `billing_reason` | What Factuarea does | | ----------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `subscription_create` (first cycle) | **Invoiced** | | `subscription_cycle` (each renewal) | **Invoiced** | | `subscription_update`, `subscription_threshold` (standalone prorations) | **Manual review** — recorded in the integration log, not auto-invoiced | | `manual`, `upcoming`, `quote_accept`, others | Ignored with an info log | * **Trials don't invoice.** A cycle whose `invoice.total` is **0 €** (a trial period, or a cycle fully covered by credit) emits no invoice; the first real charge after the trial is invoiced normally. * **Prorations standalone** (an upgrade/downgrade billed on its own, outside the regular cycle) are **not** auto-invoiced today — they go to manual review so you decide. They land in the [inbox](/payments/integration-events-inbox#reasons) as `subscription_proration_review`, which is actionable but **not** replayable: the `billing_reason` never changes, so the invoice has to be issued by hand. The regular cycle that follows is invoiced as usual. * **The F1/F2 decision is the same.** The recipient's NIF is read from the invoice's `customer_tax_ids` (plus the resolved client's file); with a NIF the cycle is an **ordinary (F1)** invoice, without one and at or below the threshold an **simplified (F2)**, and without one above the threshold (or with "require NIF" on) it goes to manual review — identical to the rules in the [decision section](#decision). The same Stripe Tax line/VAT mirroring, EUR conversion and total validation apply. ### Each cycle is its own invoice [#subscription-coexistence] A subscription cycle becomes a **standalone invoice** — it does **not** create or touch a Factuarea **recurring invoice**. The two are independent: Stripe drives the cadence and each `invoice.paid` produces one invoice. <Callout type="warn"> **Avoid double-invoicing.** If you already model the same client's subscription as a manual **recurring invoice** in Factuarea, enabling subscription auto-invoicing for that Stripe subscription will produce **two invoices per period** — one from your recurring template and one from the Stripe cycle. Pick one source per client: either stop the manual recurring invoice or leave this toggle off for those subscriptions. </Callout> ### The outbound event [#subscription-event] A subscription-cycle invoice emits a **distinct** event, [`invoice.subscription_auto_created`](/api-reference/events/public-api.v1.events.list) (plus `payment.received`), so a webhook receiver can tell subscription cycles apart from one-shot charges. A one-shot charge keeps emitting `invoice.auto_created` as before. The auto-invoiced charges listing ([List auto-invoiced charges](/api-reference/stripe/public-api.v1.stripe_autoinvoicing.payments.list)) exposes the subscription context (`subscription_id`, `stripe_invoice_id`, `period_start`, `period_end`) for cycle charges (`null` for one-shot), and an optional `origin` filter (`subscription`/`oneshot`). That same context also travels **on the invoice itself**, as system metadata keys you can filter listings by. See [Metadata and reconciliation](/payments/metadata-reconciliation) for the whole table — which keys are always written, which are omitted when they don't apply — and two ready-made recipes: every invoice of one subscription, and the invoices of a single billing period. <Callout type="info"> **Idempotency is per billing cycle.** Factuarea keys on each Stripe invoice id (`in_xxx`): a redelivered event, or a second event for the same cycle, produces **at most one** invoice. </Callout> <Callout type="info"> **A cycle that arrived while the toggle was off can still be invoiced — for 30 days.** It is not silently dropped: it is recorded in the [inbox](/payments/integration-events-inbox#reasons) as `subscription_autoinvoicing_disabled`, with its content kept encrypted. Turn `subscription_autoinvoicing_enabled` (and `enabled`) on and [replay](/payments/integration-events-inbox#replay) the event within that window, and the cycle's **real invoice is issued** — with its series number and its VeriFactu record, exactly as if it had been invoiced at the time. After 30 days the content is purged, the record remains, and the only route left is issuing the invoice by hand. Cycles billed before your Connect endpoint started sending `invoice.paid` never reached Factuarea, so there is nothing to replay for those. </Callout> <Callout type="warn"> To receive subscription cycles you must enable the **`invoice.paid`** event on your Connect webhook endpoint in the Stripe Dashboard. As always, validate the flow with a `fact_test_` key first: in the [sandbox](/guides/test-mode) the VeriFactu Alta record is created locally and never transmitted to the AEAT. </Callout> ## Defaults at a glance [#defaults] | Field | Default | Effect of the default | | ------------------------------------ | --------------- | --------------------------------------------------------------------------------------------- | | `enabled` | `false` | Auto-invoicing off — nothing is issued until you enable it | | `simplified_threshold_cents` | `40000` (400 €) | Charges without a NIF up to 400 € become F2 | | `require_nif` | `false` | Simplified invoices are allowed; tax ID is offered but not required | | `refunds_enabled` | `true` | A Stripe refund generates an automatic corrective invoice (while auto-invoicing is on) | | `subscription_autoinvoicing_enabled` | `false` | Stripe subscription cycles are not auto-invoiced until you enable it (requires `enabled` too) | With these defaults the only behaviour change once auto-invoicing is on is that a charge without a NIF up to 400 € becomes a simplified invoice instead of waiting in review, and a refund generates its corrective invoice automatically. Charges with a NIF behave exactly as before. Subscription cycles stay off until you opt in with `subscription_autoinvoicing_enabled`. --- # API pricing & limits (/pricing) The public API and the MCP server are **included in every paid Factuarea plan**. There is no add-on to buy and no access request: create a key from [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys) and start calling `/v1`. What you can pay for is **capacity**. Your plan grants a rate-limit tier; if you need a higher one without changing plans, subscribe to a [capacity boost](#capacity-boost). The price column below is the price of that boost — never the price of access. ## Tiers [#tiers] | Tier | Boost price | Per minute | Per month | Active API keys | Webhook endpoints | | ----------- | ------------------------- | ---------- | --------- | --------------- | ----------------- | | **Free** | Not for sale (trial tier) | 10 rpm | 100 | 1 | 0 | | **Starter** | €4.90 / month | 30 rpm | 5,000 | 3 | 1 | | **Pro** | €19.90 / month | 300 rpm | 50,000 | 25 | 10 | | **Scale** | Sales-led | Custom | Custom | Unlimited | Unlimited | Figures verified against the backend tier configuration on **2026-07-31**. For how the quotas behave — sliding window, `X-RateLimit-*` headers, the `429` code and the back-off strategy — see [Rate limits](/guides/rate-limits). ## What your plan already grants [#what-your-plan-already-grants] | Your plan | Tier granted at no extra cost | | ---------------------------------- | ----------------------------- | | Trial (no active subscription yet) | Free | | Emprendedor | Starter | | Empresario | Pro | | Enterprise | Scale | The tier follows the plan on its own: it is not chosen per key or per request, and it changes as soon as your plan does. ## Capacity boost [#capacity-boost] A boost buys a tier **strictly higher** than the one your plan already grants — for example Starter → Pro on the Emprendedor plan. Subscribe from [Dashboard → Developers → Upgrade](https://app.factuarea.com/settings/developers/upgrade); billing is monthly. While the boost is active, every key of the company uses the boosted tier. Because the plan already grants a tier, buying one that is equal to or lower than it is rejected — see [Rate limits → Capacity boost](/guides/rate-limits#capacity-boost) for the rule and the error it returns. ## Caps that are not requests [#caps-that-are-not-requests] Two limits are counted per company rather than per request. ### Active API keys [#active-api-keys] A key counts while it is neither revoked nor expired; revoking one frees a slot immediately. Creating one past the cap responds `422` with `code: max_api_keys_exceeded`, naming the tier and the cap. To get past it, revoke a key you no longer use or move to a higher tier. ### Webhook endpoints [#webhook-endpoints] An endpoint counts while it is live (`active` or `degraded`); a disabled or deleted endpoint does not. Creating one past the cap responds `422` with `code: business_rule_violation` and `subcode: max_webhook_endpoints_reached`. <Callout type="warn"> On the **Free** tier the cap is `0`, so the first endpoint already fails — with `402 addon_required` instead of the `422` above, because there is nothing to free up. Webhooks need a paid plan (Starter or higher) or a capacity boost. </Callout> ## Test mode is not a cheaper tier [#test-mode-is-not-a-cheaper-tier] A `fact_test_` key carries the **same tier** as your live keys, so the same per-minute and monthly quotas apply to sandbox traffic. No rate limit is waived in test mode. What the sandbox removes is the real-world effect, not the quota: VeriFactu records are created locally and **never transmitted to the AEAT**, document emails are not delivered to real recipients, and events are recorded but not delivered to your endpoints. See [Test mode & sandbox](/guides/test-mode) for the full list. One cap does behave differently: test keys live in a separate sandbox company, so they count against that company's own key allowance instead of your live one. ## High volume [#high-volume] **Scale** has no published price — the caps are agreed case by case. Write to [info@factuarea.com](mailto:info@factuarea.com) from the email associated with your company in Factuarea, with the request volume you expect and the endpoints you will hit. What the tier carries: custom per-minute and monthly quotas, unlimited active keys and webhook endpoints, a service-level agreement and a dedicated account manager. ## Checking what you consume [#checking-what-you-consume] [Dashboard → Developers → Usage](https://app.factuarea.com/settings/developers/usage) shows your consumption against the current tier. Every API response also carries the `X-RateLimit-*` headers, which is the cheapest way to spot that you are approaching the limit before you hit it — see [Rate limits](/guides/rate-limits) for how to read them. --- # SDKs overview (/sdks) Factuarea ships **official SDKs** that wrap the full v1 REST API (<Stat n="operations" /> operations across <Stat n="resources" /> resources) with a premium runtime so you don't hand‑roll HTTP: automatic retries, automatic idempotency keys, transparent cursor auto‑pagination, a typed error hierarchy, typed webhook verification and binary (PDF) downloads. <Cards> <Card icon="<Package />" title="TypeScript / Node.js" href="https://www.npmjs.com/package/@factuarea/sdk"> `@factuarea/sdk` on npm. Dual ESM + CommonJS, full type declarations. Source: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). </Card> <Card icon="<Package />" title="PHP" href="https://packagist.org/packages/factuarea/factuarea-php"> `factuarea/factuarea-php` on Packagist. PSR‑4, Guzzle‑based, PHP 8.2+. Source: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). </Card> </Cards> <Callout type="info"> **Pre‑GA (`0.x`).** Both SDKs are at `0.x`. The public method surface is stable and follows the [SDK method‑naming contract](https://github.com/factuarea), protected by SemVer — but while in `0.x`, minor versions may include breaking changes until `1.0.0`, which tracks the API's GA. Each release pins one [`Factuarea-Version`](/guides/versioning) and sends it on every request, so the API's behaviour stays stable until you upgrade the SDK. </Callout> <Callout type="warn"> **Server‑side only.** Your API key is a secret. Never ship an SDK with a live key to a browser, mobile app or any public client — use the SDK from your backend. </Callout> ## Installation [#installation] <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```bash npm install @factuarea/sdk ``` Requires **Node 20 or newer**. The SDK is built on the Web `fetch` standard, so it also runs on Deno, Bun and Cloudflare Workers. </Tab> <Tab value="PHP"> ```bash composer require factuarea/factuarea-php ``` Requires **PHP 8.2 or newer** with the `json` and `mbstring` extensions (both bundled with standard PHP builds). </Tab> </Tabs> ## Authentication & environments [#authentication--environments] Pass your API key. **The key prefix selects the environment** — there is no separate flag: a `fact_test_…` key always runs against the isolated [sandbox](/guides/test-mode), a `fact_live_…` key against production. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); factuarea.environment; // "test" or "live", derived from the key prefix ``` Optional configuration: ```ts new Factuarea({ apiKey: "fact_live_…", // required baseUrl: "https://api.factuarea.com/v1", // override for staging timeout: 60_000, // per-request ms (default 60s) maxRetries: 2, // attempts after the first try factuareaVersion: "2026-06-04", // pinned API version header defaultHeaders: {}, // extra headers on every request }); ``` </Tab> <Tab value="PHP"> ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; // The key prefix selects the environment: // fact_test_… → sandbox fact_live_… → production $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); ``` `FactuareaClient::create()` is the recommended entry point: it wires Bearer authentication and registers the automatic `Idempotency-Key` behaviour for you. For advanced configuration (custom Guzzle client, custom retry policy, staging base URL) the generated builder is still available: ```php use Factuarea\Sdk\Factuarea; use Factuarea\Sdk\Models\Components\Security; $factuarea = Factuarea::builder() ->setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY'))) ->setServerURL('https://api.factuarea.com/v1') ->build(); ``` </Tab> </Tabs> ## Quickstart [#quickstart] Create a client and an invoice, then download its PDF. Every operation is reachable as `<resource>.<method>` (TypeScript) or `->{resource}->publicApiV1{Resource}{Action}` (PHP) following the naming contract — the per‑endpoint snippets in the API reference show the exact call for each operation. <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Responses are the API's `{ data: … }` envelope — read the resource off `.data`. // 1. Create a client. const { data: client } = await factuarea.clients.create({ name: "Cliente Demo SL", tax_id: "B98765432", }); // 2. Create an invoice (the API computes the totals). const { data: invoice } = await factuarea.invoices.create({ client_id: client.id, series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", issued_on: "2026-06-05", due_on: "2026-07-05", lines: [ { description: "Consultoría — junio 2026", quantity: 10, unit_price: 100, tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", }, ], }); // 3. Download the PDF (a BinaryResponse, not JSON). const pdf = await factuarea.invoices.pdf(invoice.id); await import("node:fs/promises").then((fs) => fs.writeFile("invoice.pdf", pdf.toBuffer()), ); ``` </Tab> <Tab value="PHP"> ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; use Factuarea\Sdk\Models\Components; use Brick\DateTime\LocalDate; $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); // 1. Create a client. $client = $factuarea->clients->publicApiV1ClientsCreate( new Components\CreateClientRequest( name: 'Cliente Demo SL', taxId: 'B98765432', ), ); // 2. Create an invoice (the API computes the totals). $invoice = $factuarea->invoices->publicApiV1InvoicesCreate( new Components\CreateInvoiceRequest( clientId: $client->object->data->id, seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e', issuedOn: LocalDate::parse('2026-06-05'), dueOn: LocalDate::parse('2026-07-05'), lines: [ new Components\CreateInvoiceRequestLine( description: 'Consultoría — junio 2026', quantity: 10, unitPrice: 100, taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f', ), ], ), ); // 3. Download the PDF. $pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id); file_put_contents('invoice.pdf', $pdf->bytes ?? ''); ``` </Tab> </Tabs> <Callout type="info"> Run everything with a **`fact_test_`** key first — sandbox effects (VeriFactu → AEAT, FACe, email, webhooks) are switched off. When your flow works end‑to‑end, swap the prefix to `fact_live_`. The API surface is identical in both. See [Test mode & sandbox](/guides/test-mode). </Callout> ## Runtime features [#runtime-features] Both SDKs share the same hand‑written runtime on top of the generated typed surface: * **Automatic retries** — transient failures (`429` and `5xx`, plus network errors in TypeScript) are retried with exponential backoff and jitter, honouring the `Retry-After` header. Deterministic client errors (e.g. `422` validation) are **never** retried. * **Automatic idempotency** — every mutation gets a generated `Idempotency-Key` so a retried request never double‑creates a resource. Override it per call when you want app‑level deduplication. See [Idempotency](/guides/idempotency). * **Cursor auto‑pagination** — list methods return an iterable that walks every page for you, managing `next_cursor` / `has_more`. See [Paginating with the SDK](#paginating-with-the-sdk). * **Typed errors** — the API's [error envelope](/guides/errors) maps to a typed exception hierarchy exposing `code`, `type`, `request_id` and `status`. Your API key is never included in any error message. See [Handling errors](#handling-errors). * **Webhook verification** — a constant‑time HMAC‑SHA256 verifier that honours the secret‑rotation grace window. See [Verifying webhooks](#verifying-webhooks). * **Binary downloads** — PDF and file endpoints return a binary response you turn into a Buffer / stream, not JSON. ## Paginating with the SDK [#paginating-with-the-sdk] <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> List methods return a `Page`, which is itself an async iterable: ```ts const page = await factuarea.invoices.list({ status: "paid", limit: 50 }); // (a) iterate every item across every page for await (const invoice of page) { console.log(invoice.id); } // (b) page by page page.data; // items on this page page.hasMore; // boolean page.nextCursor; // opaque cursor or null const next = await page.getNextPage(); // Page | null // (c) collect everything into an array const all = await page.toArray(); ``` </Tab> <Tab value="PHP"> The `PageIterator` helper streams every item across all pages without manual cursor handling: ```php use Factuarea\Sdk\Custom\Pagination\PageIterator; use Factuarea\Sdk\Models\Operations\PublicApiV1InvoicesListRequest; $pages = new PageIterator( fn (?string $cursor) => $factuarea->invoices->publicApiV1InvoicesList( new PublicApiV1InvoicesListRequest(startingAfter: $cursor), )->rawResponse, ); // items() yields each item as a decoded associative array. foreach ($pages->items() as $invoice) { echo $invoice['id'], PHP_EOL; } ``` </Tab> </Tabs> See [Pagination](/guides/pagination) for the underlying cursor semantics. ## Handling errors [#handling-errors] <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { FactuareaError, ValidationError, RateLimitError, } from "@factuarea/sdk"; try { await factuarea.invoices.create(body); } catch (error) { if (error instanceof ValidationError) { console.error(error.fields); // { tax_id: ["NIF inválido"], … } } else if (error instanceof RateLimitError) { console.error(error.retryAfter); // seconds to wait } else if (error instanceof FactuareaError) { console.error(error.code, error.requestId); } } ``` </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Models\Errors\ErrorThrowable; try { $factuarea->invoices->publicApiV1InvoicesCreate($body); } catch (ErrorThrowable $e) { $error = $e->container->error; echo $error->type->value; // e.g. "invalid_request_error" echo $error->code; // e.g. "parameter_invalid" echo $error->param; // e.g. "client_id" echo $error->requestId; // quote this to support } ``` </Tab> </Tabs> Branch on the stable `code`, never on the human‑facing Spanish `message`. The full catalog is in [Errors](/guides/errors). ## Verifying webhooks [#verifying-webhooks] Pass the **raw request body** (not a re‑serialized object), the `Factuarea-Signature` header and the endpoint secret: <Tabs items="['TypeScript', 'PHP']"> <Tab value="TypeScript"> ```ts import { Factuarea, WebhookSignatureError, SIGNATURE_HEADER } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Express, with express.raw({ type: "application/json" }) on the route: app.post("/webhooks/factuarea", (req, res) => { try { const event = factuarea.webhooks.verify( req.body.toString("utf8"), req.headers[SIGNATURE_HEADER.toLowerCase()] as string, process.env.FACTUAREA_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { /* … */ } res.sendStatus(200); } catch (e) { if (e instanceof WebhookSignatureError) return res.sendStatus(400); throw e; } }); ``` </Tab> <Tab value="PHP"> ```php use Factuarea\Sdk\Custom\Webhooks\WebhookVerifier; use Factuarea\Sdk\Custom\Webhooks\WebhookSignatureException; $verifier = new WebhookVerifier(); $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_FACTUAREA_SIGNATURE'] ?? ''; try { $event = $verifier->verify($rawBody, $signature, getenv('FACTUAREA_WEBHOOK_SECRET')); // $event is the decoded, authenticated payload } catch (WebhookSignatureException $e) { http_response_code(400); } ``` </Tab> </Tabs> Verification uses HMAC‑SHA256 with a constant‑time comparison and a configurable timestamp tolerance (default 5 minutes) to reject replays, and accepts both signatures during a secret‑rotation grace window. See [Webhooks](/guides/webhooks). ## Per‑endpoint snippets [#perendpoint-snippets] Every page in the API reference shows a ready‑to‑copy **TypeScript**, **PHP** and **cURL** snippet for that exact operation, generated from the spec so they never drift from the live surface. ## Generate your own client [#generate-your-own-client] If your language isn't covered yet, or you prefer a client you own and check into your repo, the canonical machine contract is the **OpenAPI 3.1** spec — point any generator at it. <Callout type="info"> The spec lives at [`https://docs.factuarea.com/api/openapi`](/api/openapi). It is generated from the same backend that serves the API, so it never drifts from the live surface. </Callout> <Tabs items="['openapi-typescript', 'Python', 'OpenAPI Generator']"> <Tab value="openapi-typescript"> ```bash npx openapi-typescript https://docs.factuarea.com/api/openapi \ -o src/factuarea.d.ts ``` </Tab> <Tab value="Python"> ```bash openapi-python-client generate \ --url https://docs.factuarea.com/api/openapi ``` </Tab> <Tab value="OpenAPI Generator"> ```bash openapi-generator-cli generate \ -i https://docs.factuarea.com/api/openapi \ -g <language> -o ./factuarea-client ``` `<language>` can be any [supported generator](https://openapi-generator.tech/docs/generators) — Go, Java, C#, Ruby, Rust and more. </Tab> </Tabs> A generated client won't include the official SDK's runtime (retries, idempotency, pagination, webhook verification) — you wire those yourself following the [core concept guides](/guides/idempotency). ## Building with an AI assistant? [#building-with-an-ai-assistant] If you want an AI agent to operate Factuarea directly rather than generate client code, connect it to the [MCP server](/mcp) — the public API exposed as tools, with OAuth and API-key auth. For Claude Code, the official `factuarea-mcp` [plugin](/mcp/claude-code-plugin) wires it up in two commands. --- # PHP (/sdks/php) The official PHP SDK is [`factuarea/factuarea-php`](https://packagist.org/packages/factuarea/factuarea-php) on Packagist — PSR-4, Guzzle-based. Source: [github.com/factuarea/factuarea-php](https://github.com/factuarea/factuarea-php). It wraps the v1 REST API with automatic retries, idempotency keys, cursor auto-pagination, a typed error hierarchy and webhook verification — covered in the [SDK overview](/sdks). ## Install [#install] ```bash composer require factuarea/factuarea-php ``` Requires **PHP 8.2 or newer** with the `json` and `mbstring` extensions (both bundled with standard PHP builds). ## Authenticate [#authenticate] Pass your API key. **The key prefix selects the environment** — there is no separate flag: a `fact_test_…` key always runs against the isolated [sandbox](/guides/test-mode), a `fact_live_…` key against production. ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; // The key prefix selects the environment: // fact_test_… → sandbox fact_live_… → production $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); ``` `FactuareaClient::create()` is the recommended entry point: it wires Bearer authentication and registers the automatic `Idempotency-Key` behaviour for you. For advanced configuration (custom Guzzle client, custom retry policy, staging base URL) the generated builder is still available: ```php use Factuarea\Sdk\Factuarea; use Factuarea\Sdk\Models\Components\Security; $factuarea = Factuarea::builder() ->setSecurity(new Security(bearerAuth: getenv('FACTUAREA_API_KEY'))) ->setServerURL('https://api.factuarea.com/v1') ->build(); ``` <Callout type="warn"> **Server-side only.** Your API key is a secret. Never ship the SDK with a live key to a public client — use it from your backend. </Callout> ## Quickstart [#quickstart] Create a client and an invoice, then download its PDF. Every operation is reachable as `->{resource}->publicApiV1{Resource}{Action}`; the per-endpoint snippets in the API reference show the exact call for each operation. ```php <?php require 'vendor/autoload.php'; use Factuarea\Sdk\Custom\FactuareaClient; use Factuarea\Sdk\Models\Components; use Brick\DateTime\LocalDate; $factuarea = FactuareaClient::create(getenv('FACTUAREA_API_KEY')); // 1. Create a client. $client = $factuarea->clients->publicApiV1ClientsCreate( new Components\CreateClientRequest( name: 'Cliente Demo SL', taxId: 'B98765432', ), ); // 2. Create an invoice (the API computes the totals). $invoice = $factuarea->invoices->publicApiV1InvoicesCreate( new Components\CreateInvoiceRequest( clientId: $client->object->data->id, seriesId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e', issuedOn: LocalDate::parse('2026-06-05'), dueOn: LocalDate::parse('2026-07-05'), lines: [ new Components\CreateInvoiceRequestLine( description: 'Consultoría — junio 2026', quantity: 10, unitPrice: 100, taxRateId: '01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f', ), ], ), ); // 3. Download the PDF. $pdf = $factuarea->invoices->publicApiV1InvoicesPdf($invoice->object->data->id); file_put_contents('invoice.pdf', $pdf->bytes ?? ''); ``` <Callout type="info"> Run everything with a **`fact_test_`** key first — sandbox effects (VeriFactu → AEAT, FACe, email, webhooks) are switched off. When your flow works end-to-end, swap the prefix to `fact_live_`. See [Test mode & sandbox](/guides/test-mode). </Callout> ## Next steps [#next-steps] The runtime behaviour — retries, idempotency, cursor auto-pagination, the typed error hierarchy and webhook verification — is shared across both SDKs and documented once in the [SDK overview](/sdks): * [Runtime features](/sdks#runtime-features) * [Paginating with the SDK](/sdks#paginating-with-the-sdk) * [Handling errors](/sdks#handling-errors) * [Verifying webhooks](/sdks#verifying-webhooks) --- # TypeScript (/sdks/typescript) The official TypeScript SDK is [`@factuarea/sdk`](https://www.npmjs.com/package/@factuarea/sdk) on npm — dual ESM + CommonJS with full type declarations. Source: [github.com/factuarea/factuarea-node](https://github.com/factuarea/factuarea-node). It wraps the v1 REST API with automatic retries, idempotency keys, cursor auto-pagination, a typed error hierarchy and webhook verification — covered in the [SDK overview](/sdks). ## Install [#install] ```bash npm install @factuarea/sdk ``` Requires **Node 20 or newer**. The SDK is built on the Web `fetch` standard, so it also runs on Deno, Bun and Cloudflare Workers. ## Authenticate [#authenticate] Pass your API key. **The key prefix selects the environment** — there is no separate flag: a `fact_test_…` key always runs against the isolated [sandbox](/guides/test-mode), a `fact_live_…` key against production. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); factuarea.environment; // "test" or "live", derived from the key prefix ``` Optional configuration: ```ts new Factuarea({ apiKey: "fact_live_…", // required baseUrl: "https://api.factuarea.com/v1", // override for staging timeout: 60_000, // per-request ms (default 60s) maxRetries: 2, // attempts after the first try factuareaVersion: "2026-06-04", // pinned API version header defaultHeaders: {}, // extra headers on every request }); ``` <Callout type="warn"> **Server-side only.** Your API key is a secret. Never ship the SDK with a live key to a browser, mobile app or any public client — use it from your backend. </Callout> ## Quickstart [#quickstart] Create a client and an invoice, then download its PDF. Every operation is reachable as `<resource>.<method>`; the per-endpoint snippets in the API reference show the exact call for each operation. ```ts import { Factuarea } from "@factuarea/sdk"; const factuarea = new Factuarea({ apiKey: process.env.FACTUAREA_API_KEY! }); // Responses are the API's `{ data: … }` envelope — read the resource off `.data`. // 1. Create a client. const { data: client } = await factuarea.clients.create({ name: "Cliente Demo SL", tax_id: "B98765432", }); // 2. Create an invoice (the API computes the totals). const { data: invoice } = await factuarea.invoices.create({ client_id: client.id, series_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0e", issued_on: "2026-06-05", due_on: "2026-07-05", lines: [ { description: "Consultoría — junio 2026", quantity: 10, unit_price: 100, tax_rate_id: "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0f", }, ], }); // 3. Download the PDF (a BinaryResponse, not JSON). const pdf = await factuarea.invoices.pdf(invoice.id); await import("node:fs/promises").then((fs) => fs.writeFile("invoice.pdf", pdf.toBuffer()), ); ``` <Callout type="info"> Run everything with a **`fact_test_`** key first — sandbox effects (VeriFactu → AEAT, FACe, email, webhooks) are switched off. When your flow works end-to-end, swap the prefix to `fact_live_`. See [Test mode & sandbox](/guides/test-mode). </Callout> ## Next steps [#next-steps] The runtime behaviour — retries, idempotency, cursor auto-pagination, the typed error hierarchy and webhook verification — is shared across both SDKs and documented once in the [SDK overview](/sdks): * [Runtime features](/sdks#runtime-features) * [Paginating with the SDK](/sdks#paginating-with-the-sdk) * [Handling errors](/sdks#handling-errors) * [Verifying webhooks](/sdks#verifying-webhooks) --- # Support (/support) This page is the single source of truth for how to reach the Factuarea API team and what to send so we can help you quickly. ## Contact [#contact] <Cards> <Card icon="<Mail />" title="info@factuarea.com" href="mailto:info@factuarea.com"> The channel for everything API-related: integration questions, bug reports and incidents. </Card> </Cards> Write from the email associated with your company in Factuarea. ## What to include when reporting an issue [#what-to-include-when-reporting-an-issue] Every API response carries a unique `request_id` (in the error envelope under `error.request_id`, and in the `X-Request-Id` response header). It is the single most useful thing you can send us — it lets us correlate logs, metrics and traces to investigate quickly. ```json { "error": { "type": "invalid_request_error", "code": "parameter_invalid", "message": "El campo client_id es obligatorio.", "request_id": "req_01HKQS5N8VR7QXJ9K3T6BWPMZA" } } ``` A good report includes: * **`request_id`** of the failing call (or several, if it's intermittent). * **HTTP status** and the `type` / `code` from the error envelope. * **Endpoint and method** — e.g. `POST /v1/invoices`. * **Environment** — `live` or `test` (the prefix of the key you used, `fact_live_` or `fact_test_`). Never paste the key secret itself. * **What you expected** vs. what happened, and the approximate timestamp. <Callout type="warn"> Never share an API key secret in a support email. Send the `request_id` — we can find the key and request from that alone. If a secret has been exposed, [rotate or revoke the key](/guides/authentication) from the dashboard first. </Callout> A subject line that already carries the essentials helps us triage: ``` 422 on POST /v1/invoices — request_id req_01JBVH7K9Y4N3CDQ2EHJB1AGSV ``` ## API access [#api-access] The public API and the MCP server are **included in every Factuarea plan** — there is no beta program and no separate add-on. Create your keys from [Dashboard → Developers → API Keys](https://app.factuarea.com/settings/developers/api-keys); your rate-limit tier is derived from your plan (see [Rate limits](/guides/rate-limits)). If your calls return `403 addon_not_active`, your company has no active plan that includes API access — subscribe to or renew a plan from the dashboard. ## Status page [#status-page] A public status page (uptime and incident history) will live at **status.factuarea.com**. Until then, we notify affected companies of incidents and planned maintenance directly by email to keys' registered contacts. ## Changelog [#changelog] Every `/v1` change — new fields, new endpoints, new events, validation fixes and deprecations — is published in the [Changelog](/changelog/launch). Breaking changes never land in `/v1`; they only appear in a future `/v2`. See [Versioning](/guides/versioning) for the stability commitment. ## Self-service first [#self-service-first] Before opening a ticket, these usually answer the question faster: * [FAQ](/faq) — the most common integration questions. * [Errors](/guides/errors) — look up your `code` for the cause and fix. * [Authentication](/guides/authentication) — keys, scopes, rotation. * [Rate limits](/guides/rate-limits) — quotas and back-off.