# Update an invoice series API Reference

Updates only the fields present in the body, leaving every other field of the series as
it is.

- **Clearing a field:** a field sent as `null` is cleared, which only `description`
  supports.
- **Numbering fields:** `code`, `format`, `counter_reset` and `initial_number` are rejected
  once the series has issued invoices (`numbering_locked` is `true`).
- **Unique numbers per issuer:** changing `code`, `format` or `document_type` so that the
  series could print a number another series of the company can also print is rejected
  with `409 SERIES_FORMAT_OVERLAPS`.
- **`default_series`:** it cannot be used to clear the default. Sending `false` for the
  series that currently is the default answers `DEFAULT_CANNOT_BE_UNMARKED`, because it
  would leave the document type with active series and no default, and issuing without an
  explicit `series_id` would then fail with `SERIES_DEFAULT_NOT_FOUND`. Hand the default
  over with `PUT /v1/companies/{company_id}/series/{series_id}/default` on the new series,
  which unmarks the previous one. Sending `false` for a series that is not the default is a
  no-op.


## PATCH /v1/companies/{company_id}/series/{series_id}

**Update an invoice series**

Updates only the fields present in the body, leaving every other field of the series as
it is.

- **Clearing a field:** a field sent as `null` is cleared, which only `description`
  supports.
- **Numbering fields:** `code`, `format`, `counter_reset` and `initial_number` are rejected
  once the series has issued invoices (`numbering_locked` is `true`).
- **Unique numbers per issuer:** changing `code`, `format` or `document_type` so that the
  series could print a number another series of the company can also print is rejected
  with `409 SERIES_FORMAT_OVERLAPS`.
- **`default_series`:** it cannot be used to clear the default. Sending `false` for the
  series that currently is the default answers `DEFAULT_CANNOT_BE_UNMARKED`, because it
  would leave the document type with active series and no default, and issuing without an
  explicit `series_id` would then fail with `SERIES_DEFAULT_NOT_FOUND`. Hand the default
  over with `PUT /v1/companies/{company_id}/series/{series_id}/default` on the new series,
  which unmarks the previous one. Sending `false` for a series that is not the default is a
  no-op.

### Authentication

Accepts any of:

- `ApiKeyAuth` (HTTP bearer, token format `beel_sk_*`)

### Parameters

- **company_id** (required) in path `string`: Unique identifier (UUID) of the company the operation acts on — its identifier, not its NIF. It is the only source of context: the account that owns it is derived from it, and the `BeeL-Active-Company` header plays no part. A company you do not reach answers `403`, and so does a company that does not exist, so the existence of a company in another account is never disclosed.
- **series_id** (required) in path: Series ID

### Request Body

Required.

**Content `application/json`:**

- **document_type** `DocumentType`: Document type associated with a series. Values mirror `InvoiceType`, so the series a document needs is named exactly like the document: - UNASSIGNED: Legacy value of series created before types existed. A series numbers only documents of its own type, so an `UNASSIGNED` series numbers none (`422 SERIES_INCOMPATIBLE_DOC_TYPE`); give it a type to keep using it. No series can be created with it or moved to it (`422 SERIES_UNASSIGNED_TYPE_NOT_ALLOWED`). The live ones were given the type they numbered most. - STANDARD: Standard invoice - SIMPLIFIED: Simplified invoice - CORRECTIVE: Corrects or cancels a previous standard invoice (R1–R4) - SIMPLIFIED_CORRECTIVE: Corrects a simplified invoice (R5). Spanish law requires R5 to be numbered apart from R1–R4, so they have a series type of their own - PROFORMA: Proforma (commercial document, non-fiscal numbering)
- **name** `string`: Descriptive name of the series. Cannot be cleared. (example: "Main Series")
- **code** `SeriesCode`: Alphanumeric series code (used in {CODIGO} variable). Allows uppercase letters, numbers, hyphens and underscores.
- **description** `string`: Series description. Send `null` to clear it. (example: "Series for standard invoices")
- **format** `SeriesFormat`: Format template with available variables (UPPERCASE ONLY): - {CODIGO}: Series code (e.g., "FAC") - {YYYY}: Year with 4 digits (e.g., "2025") - {YY}: Year with 2 digits (e.g., "25") - {MM}: Month with 2 digits (e.g., "01") - {NUM}: Sequential number without padding (e.g., "1") - {NUM:X}: Sequential number with padding (e.g., {NUM:4} → "0001") **REQUIRED**: Must contain at least {NUM} or {NUM:X} **IMPORTANT**: Only uppercase (rejects {yy}, {mm}, {codigo}, etc.) Valid examples: - "{CODIGO}-{YYYY}-{NUM:4}" → "FAC-2025-0001" - "{CODIGO}/{NUM:6}" → "FAC/000001" - "{YYYY}{MM}-{NUM:3}" → "202501-001" The generated number is the invoice number sent to the AEAT, which accepts at most 60 printable ASCII characters and none of `"`, `'`, `<`, `>`, `=`. A format whose longest possible number breaks that rule is rejected with `422 SERIES_FORMAT_NUMBER_TOO_LONG` or `SERIES_FORMAT_INVALID_CHARACTERS`. The counter counts as at least 9 digits, with or without padding: `{NUM:X}` is a minimum width, not a maximum.
- **counter_reset** `CounterReset`: Counter reset policy: - NEVER: Counter never resets (continuous numbering) - ANNUAL: Counter resets yearly - MONTHLY: Counter resets monthly
- **initial_number** `integer` (int64): Initial number for this series counter. Only while the series has no issued invoices. It applies only to the first period in which the series issues an invoice: with `counter_reset: ANNUAL` or `MONTHLY`, every later year or month starts at 1. With `NEVER` there is a single period, so numbering simply continues from it. (example: 54)
- **active** `boolean`: Whether the series is active. **Restriction:** A default series cannot be deactivated (another must be set as default first).
- **default_series** `boolean`: Whether this is the default series. **Restriction:** An inactive series cannot be marked as default.

**Example `update_series_description`** — Update description:

```json
{
  "name": "Serie Principal Actualizada",
  "description": "Default series for domestic standard invoices - Updated 2025"
}
```

**Example `update_series_format`** — Change numbering format:

```json
{
  "format": "{YYYY}-{CODIGO}-{NUM:4}"
}
```

### Responses

#### 200: Series updated successfully

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: true)
- **data** (required): The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
- **meta** `ResponseMeta`
- **data** `InvoiceSeries`

#### 400: The update is rejected. Error codes:
- `SERIES_CODE_LOCKED_HAS_INVOICES`, `SERIES_FORMAT_LOCKED_HAS_INVOICES`,
  `SERIES_RESET_LOCKED_HAS_INVOICES`, `SERIES_INITIAL_NUMBER_LOCKED_HAS_INVOICES`,
  `SERIES_DOCUMENT_TYPE_LOCKED_HAS_INVOICES` — the series already consumed a number
  (`numbering_locked` is `true`)
- `DEFAULT_CANNOT_BE_UNMARKED` — `default_series: false` on the series that
  currently is the default; promote another one with
  `PUT .../series/{series_id}/default` instead
- `DEFAULT_CANNOT_DEACTIVATE` — `active: false` on the default series
- `INACTIVE_CANNOT_BE_DEFAULT` — `default_series: true` on an inactive series
- `REFERENCED_BY_PAYMENT_CONNECTION` — deactivating a series a payment
  connection is configured to use
- `REFERENCED_BY_RECURRING_INVOICE` — deactivating a series with a live
  (ACTIVE or PAUSED) recurring invoice template using it. Finish it or switch it
  to another series first
- `SERIES_NUMBERING_FROZEN` — the series issued its first number while this change
  was being applied, so its numbering can no longer change. Read it again: the
  `*_LOCKED_HAS_INVOICES` codes above name the field


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 401: Missing or invalid authentication. Like every other error, `message`/`detail` is
localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the
header is missing or asks for none of those.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication is required to access this resource"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 403: Your account does not own or manage this company, or does not hold the required
access over it. A NIF that does not exist answers the same way.
That case answers `error.code` `ACTIVE_COMPANY_NOT_ACCESSIBLE`.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 404: No series with that id under this company. A series belonging to another NIF answers
the same way: under the scope of the NIF in the path it simply does not exist.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 409: `SERIES_FORMAT_OVERLAPS` — with the change, the series could print numbers another series
of the company prints. `SERIES_DEFAULT_CONFLICT` — another request made a different
series the default for the same document type at the same time; read the series
again and retry.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 422: The series is rejected. `error.code` is `VALIDATION_ERROR` when the body parses but a value
is not acceptable: `details` is a flat map from the offending property's name (in the
contract's `snake_case`) to a message describing what is wrong with it, or follows
`FieldDeserializationError` when the value is outside an enum's vocabulary. Otherwise it is
one of the series rules, among them:
- `SERIES_MONTHLY_REQUIRES_MONTH_AND_YEAR` / `SERIES_ANNUAL_REQUIRES_YEAR` — the format
  cannot tell the counter reset periods apart.
- `SERIES_INITIAL_NUMBER_OUT_OF_RANGE` — the initial number is out of range.
- `SERIES_FORMAT_NUMBER_TOO_LONG` — the longest number the series can generate (the
  format with the code, the year, the month and the counter substituted) exceeds the 60
  characters the AEAT accepts for the invoice number. The counter counts as at least 9
  digits, with or without padding: `{NUM:X}` is a minimum width and the number keeps
  growing past it.
- `SERIES_FORMAT_INVALID_CHARACTERS` — that number would contain a character the AEAT does
  not accept: only printable ASCII (32-126) is allowed, except `"`, `'`, `<`, `>` and `=`.
- `SERIES_TYPE_CHANGE_LEAVES_TYPE_UNCOVERED` — changing `document_type` of the default
  series would leave its current document type, which still has other active series,
  without a default. Make one of those the default first.
- `SERIES_COMPANY_DELETED` — the company was deleted while the request was running: its
  series keep their numbering but can no longer be activated or made the default.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "SERIES_FORMAT_NUMBER_TOO_LONG",
    "message": "The number this series generates can reach 64 characters and the AEAT accepts at most 60 (series and number together). Shorten the code or the fixed text of the format"
  },
  "meta": {
    "timestamp": "2026-09-24T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 429: Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is
over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because
too many went out to that recipient in a short time. Either way, retry after the
`Retry-After` seconds.

`QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan
for the current period (issued invoices, for instance). Retrying does not help until the
quota resets or the plan changes. `error.details` carries `feature_code`,
`current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until
the reset when it is known.


**Headers:**

- `Retry-After` `integer`: Seconds until the rate limit resets
- `RateLimit-Limit` `integer`: Maximum requests allowed in the window
- `RateLimit-Remaining` `integer`: Remaining requests in the current window
- `RateLimit-Reset` `integer`: Seconds until the current window resets

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please try again in 60 seconds."
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 500: Internal server error

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### default: Any status code the operation does not list above. Every operation declares it, so a
generated client always has a branch to fall into and never loses the cause of a failure
it did not anticipate.

This is where the transport-level answers land — `405`, `406`, `415` and `429` — together
with any status a future version of the API starts returning. All of them carry the same
error envelope as the codes listed explicitly, so `error.code` is what tells them apart:
switching on the status code alone is not enough. See «Transport-level errors» in the
API description for when each one is produced.

A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active
subscription. `error.details` says why and what to do: `current_status` (absent if it
never had one), `required_permission` and `action`.

A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a
dependency of BeeL itself (the database, the email provider) was momentarily
unavailable and nothing was done. Retry after those seconds.

A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the
operation depends on failed or did not answer in time. It is a transient condition — retry
with the same `Idempotency-Key` where the operation accepts one.

One exception to the envelope: a failure of the network edge that never reaches the
application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not
BeeL's — it may not even be JSON. Treat those as "no answer", and retry.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "UNSUPPORTED_MEDIA_TYPE",
    "message": "Unsupported media type: text/plain. Supported: application/json"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

---

# Related Schema Definitions

## PatchSeriesRequest

Partial update of an invoice series (RFC 5789). Only the fields present in the
body are touched:

- **field omitted** → the current value is kept;
- **field sent with a value** → replaced;
- **field sent as `null`** → cleared (only `description`, the one field a series
  can live without).

The same rules as `PUT` apply: the fields that drive numbering (`code`, `format`,
`counter_reset`, `initial_number`) and `document_type` are rejected once the series has
issued invoices (`document_type` can still be set on an `UNASSIGNED` series), so renaming a
series in production keeps working.
Moving a series to `document_type: UNASSIGNED` is rejected with
`422 SERIES_UNASSIGNED_TYPE_NOT_ALLOWED`; a series that already is `UNASSIGNED` can be
edited without changing its type.

- **document_type** `DocumentType`: Document type associated with a series. Values mirror `InvoiceType`, so the series a document needs is named exactly like the document: - UNASSIGNED: Legacy value of series created before types existed. A series numbers only documents of its own type, so an `UNASSIGNED` series numbers none (`422 SERIES_INCOMPATIBLE_DOC_TYPE`); give it a type to keep using it. No series can be created with it or moved to it (`422 SERIES_UNASSIGNED_TYPE_NOT_ALLOWED`). The live ones were given the type they numbered most. - STANDARD: Standard invoice - SIMPLIFIED: Simplified invoice - CORRECTIVE: Corrects or cancels a previous standard invoice (R1–R4) - SIMPLIFIED_CORRECTIVE: Corrects a simplified invoice (R5). Spanish law requires R5 to be numbered apart from R1–R4, so they have a series type of their own - PROFORMA: Proforma (commercial document, non-fiscal numbering)
- **name** `string`: Descriptive name of the series. Cannot be cleared. (example: "Main Series")
- **code** `SeriesCode`: Alphanumeric series code (used in {CODIGO} variable). Allows uppercase letters, numbers, hyphens and underscores.
- **description** `string`: Series description. Send `null` to clear it. (example: "Series for standard invoices")
- **format** `SeriesFormat`: Format template with available variables (UPPERCASE ONLY): - {CODIGO}: Series code (e.g., "FAC") - {YYYY}: Year with 4 digits (e.g., "2025") - {YY}: Year with 2 digits (e.g., "25") - {MM}: Month with 2 digits (e.g., "01") - {NUM}: Sequential number without padding (e.g., "1") - {NUM:X}: Sequential number with padding (e.g., {NUM:4} → "0001") **REQUIRED**: Must contain at least {NUM} or {NUM:X} **IMPORTANT**: Only uppercase (rejects {yy}, {mm}, {codigo}, etc.) Valid examples: - "{CODIGO}-{YYYY}-{NUM:4}" → "FAC-2025-0001" - "{CODIGO}/{NUM:6}" → "FAC/000001" - "{YYYY}{MM}-{NUM:3}" → "202501-001" The generated number is the invoice number sent to the AEAT, which accepts at most 60 printable ASCII characters and none of `"`, `'`, `<`, `>`, `=`. A format whose longest possible number breaks that rule is rejected with `422 SERIES_FORMAT_NUMBER_TOO_LONG` or `SERIES_FORMAT_INVALID_CHARACTERS`. The counter counts as at least 9 digits, with or without padding: `{NUM:X}` is a minimum width, not a maximum.
- **counter_reset** `CounterReset`: Counter reset policy: - NEVER: Counter never resets (continuous numbering) - ANNUAL: Counter resets yearly - MONTHLY: Counter resets monthly
- **initial_number** `integer` (int64): Initial number for this series counter. Only while the series has no issued invoices. It applies only to the first period in which the series issues an invoice: with `counter_reset: ANNUAL` or `MONTHLY`, every later year or month starts at 1. With `NEVER` there is a single period, so numbering simply continues from it. (example: 54)
- **active** `boolean`: Whether the series is active. **Restriction:** A default series cannot be deactivated (another must be set as default first).
- **default_series** `boolean`: Whether this is the default series. **Restriction:** An inactive series cannot be marked as default.

## DocumentType

Document type associated with a series. Values mirror `InvoiceType`,
so the series a document needs is named exactly like the document:
- UNASSIGNED: Legacy value of series created before types existed. A series numbers only
  documents of its own type, so an `UNASSIGNED` series numbers none
  (`422 SERIES_INCOMPATIBLE_DOC_TYPE`); give it a type to keep using it. No series can be
  created with it or moved to it (`422 SERIES_UNASSIGNED_TYPE_NOT_ALLOWED`). The live
  ones were given the type they numbered most.
- STANDARD: Standard invoice
- SIMPLIFIED: Simplified invoice
- CORRECTIVE: Corrects or cancels a previous standard invoice (R1–R4)
- SIMPLIFIED_CORRECTIVE: Corrects a simplified invoice (R5). Spanish law requires R5 to be
  numbered apart from R1–R4, so they have a series type of their own
- PROFORMA: Proforma (commercial document, non-fiscal numbering)

Type: `string` — one of: UNASSIGNED, STANDARD, SIMPLIFIED, CORRECTIVE, SIMPLIFIED_CORRECTIVE, PROFORMA

## SeriesCode

Alphanumeric series code (used in {CODIGO} variable).
Allows uppercase letters, numbers, hyphens and underscores.

Type: `string`

## SeriesFormat

Format template with available variables (UPPERCASE ONLY):
- {CODIGO}: Series code (e.g., "FAC")
- {YYYY}: Year with 4 digits (e.g., "2025")
- {YY}: Year with 2 digits (e.g., "25")
- {MM}: Month with 2 digits (e.g., "01")
- {NUM}: Sequential number without padding (e.g., "1")
- {NUM:X}: Sequential number with padding (e.g., {NUM:4} → "0001")

**REQUIRED**: Must contain at least {NUM} or {NUM:X}
**IMPORTANT**: Only uppercase (rejects {yy}, {mm}, {codigo}, etc.)

Valid examples:
- "{CODIGO}-{YYYY}-{NUM:4}" → "FAC-2025-0001"
- "{CODIGO}/{NUM:6}" → "FAC/000001"
- "{YYYY}{MM}-{NUM:3}" → "202501-001"

The generated number is the invoice number sent to the AEAT, which accepts at most 60
printable ASCII characters and none of `"`, `'`, `<`, `>`, `=`. A format whose longest
possible number breaks that rule is rejected with `422 SERIES_FORMAT_NUMBER_TOO_LONG` or
`SERIES_FORMAT_INVALID_CHARACTERS`. The counter counts as at least 9 digits, with or without
padding: `{NUM:X}` is a minimum width, not a maximum.

Type: `string`

## CounterReset

Counter reset policy:
- NEVER: Counter never resets (continuous numbering)
- ANNUAL: Counter resets yearly
- MONTHLY: Counter resets monthly

Type: `string` — one of: NEVER, ANNUAL, MONTHLY

## SuccessResponse

The envelope every successful JSON response of the BeeL. API is wrapped in. There are no
bare resources in v1 and none are planned: the payload always hangs off `data`.

- A **single resource** is an object in `data`.
- A **collection** hangs off a named key inside `data`, together with its `pagination`,
  also inside `data` — `data: {invoices: [...], pagination: {...}}`.
- A collection carries `pagination` unless its operation declares itself a **closed
  catalogue**: a fixed, bounded list with nothing to page through. The declaration is
  explicit in the operation; a missing `pagination` is never something to infer.
- `GET /v1/accounts` pages by cursor (`data: {accounts: [...], next_cursor}`). It is a
  documented variant of pagination, not another envelope.

Putting the array straight into `data` with `pagination` as its sibling is the shape a v2
would adopt; v1 is not being flipped to it.

- **success** (required) `boolean`: No description (example: true)
- **data** (required): The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
- **meta** `ResponseMeta`

## ResponseMeta

- **timestamp** `string` (date-time): No description (example: "2025-01-15T10:30:00Z")
- **request_id** `string`: No description (example: "4bf92f3577b34da6a3ce929d0e0e4736")

## InvoiceSeries

- **id** (required) `string` (uuid): Unique series ID
- **document_type** (required) `DocumentType`: Document type associated with a series. Values mirror `InvoiceType`, so the series a document needs is named exactly like the document: - UNASSIGNED: Legacy value of series created before types existed. A series numbers only documents of its own type, so an `UNASSIGNED` series numbers none (`422 SERIES_INCOMPATIBLE_DOC_TYPE`); give it a type to keep using it. No series can be created with it or moved to it (`422 SERIES_UNASSIGNED_TYPE_NOT_ALLOWED`). The live ones were given the type they numbered most. - STANDARD: Standard invoice - SIMPLIFIED: Simplified invoice - CORRECTIVE: Corrects or cancels a previous standard invoice (R1–R4) - SIMPLIFIED_CORRECTIVE: Corrects a simplified invoice (R5). Spanish law requires R5 to be numbered apart from R1–R4, so they have a series type of their own - PROFORMA: Proforma (commercial document, non-fiscal numbering)
- **name** (required) `string`: Descriptive name of the series (example: "Main Series")
- **code** (required) `SeriesCode`: Alphanumeric series code (used in {CODIGO} variable). Allows uppercase letters, numbers, hyphens and underscores.
- **description** `string`: Optional series description (example: "Series for standard invoices")
- **format** (required) `SeriesFormat`: Format template with available variables (UPPERCASE ONLY): - {CODIGO}: Series code (e.g., "FAC") - {YYYY}: Year with 4 digits (e.g., "2025") - {YY}: Year with 2 digits (e.g., "25") - {MM}: Month with 2 digits (e.g., "01") - {NUM}: Sequential number without padding (e.g., "1") - {NUM:X}: Sequential number with padding (e.g., {NUM:4} → "0001") **REQUIRED**: Must contain at least {NUM} or {NUM:X} **IMPORTANT**: Only uppercase (rejects {yy}, {mm}, {codigo}, etc.) Valid examples: - "{CODIGO}-{YYYY}-{NUM:4}" → "FAC-2025-0001" - "{CODIGO}/{NUM:6}" → "FAC/000001" - "{YYYY}{MM}-{NUM:3}" → "202501-001" The generated number is the invoice number sent to the AEAT, which accepts at most 60 printable ASCII characters and none of `"`, `'`, `<`, `>`, `=`. A format whose longest possible number breaks that rule is rejected with `422 SERIES_FORMAT_NUMBER_TOO_LONG` or `SERIES_FORMAT_INVALID_CHARACTERS`. The counter counts as at least 9 digits, with or without padding: `{NUM:X}` is a minimum width, not a maximum.
- **counter_reset** (required) `CounterReset`: Counter reset policy: - NEVER: Counter never resets (continuous numbering) - ANNUAL: Counter resets yearly - MONTHLY: Counter resets monthly
- **initial_number** `integer` (int64): Initial number configured for this series counter: the number the series starts from. It applies only to the first period in which the series issues an invoice: with `counter_reset: ANNUAL` or `MONTHLY`, every later year or month starts at 1. With `NEVER` there is a single period, so numbering simply continues from it. (example: 1)
- **active** (required) `boolean`: Indicates whether the series is active
- **default_series** (required) `boolean`: Indicates whether this is the user's default series (only one can be)
- **numbering_locked** `boolean`: `true` once this series has issued at least one invoice. From that point the fields that drive numbering (`code`, `format`, `counter_reset`, `initial_number`) are permanently locked and any attempt to change them is rejected — numbering can only be adjusted **before** the first invoice of a series. Cosmetic fields (`name`, `description`) remain editable. To use a different numbering, create a new series.
- **created_at** `string` (date-time): Creation date
- **next_number** `integer` (int64): Next invoice number that will be assigned for this series
- **updated_at** `string` (date-time): Last update date

## ErrorResponse

Error response shared by all BeeL. APIs.

The payload carries **two contracts at once** (additive, non-breaking):

- **Legacy** (`success`, `error.{code,message,details}`, `meta`) — kept
  intact for existing consumers.
- **RFC 9457** (`type`, `title`, `detail`, `instance`) — new fields
  for integrators following Problem Details for HTTP APIs. The
  `type` URI is the stable, shareable link to the error's
  documentation page (e.g. `https://docs.beel.es/errors/{code}`).

Future migration: the legacy fields will be deprecated via
`Deprecation`/`Sunset` headers after a sufficient adoption window,
and the response Content-Type will move to
`application/problem+json`.

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

## ErrorDetail

- **code** (required) `string`: No description (example: "VALIDATION_ERROR")
- **message** (required) `string`: No description (example: "The provided data is not valid")
- **details** `object`: No description (example: {"field":"specific error message"})


---

Full OpenAPI spec: https://docs.beel.es/api/openapi