# Delete several customers of a company API Reference

Deletes the customers listed in `ids` from this company.

## Partial results

- **Partial operation:** the customers that can be deleted are deleted, and the rest keep
  their place in `customers_deletion` with the status that explains why. That is why it
  answers `200` with a body instead of `204`, and why it answers `200` even when no row
  could be deleted.
- **`HAS_INVOICES`:** a customer that has invoices cannot be deleted and comes back with
  that row status.

## What deleting means

- **Semantics:** the same semantics as
  `DELETE /v1/companies/{company_id}/customers/{customer_id}` — the customer is retained
  internally for tax record-keeping purposes but is no longer exposed by the API, its
  identifier is released for reuse, and invoices already issued to it keep their own copy of
  the recipient's details.
- **Deleting is not deactivating:** deleting frees the identifier, so the same NIF can be
  registered again, while `PATCH` with `active: false` leaves the customer where it is with
  its NIF still taken.


## DELETE /v1/companies/{company_id}/customers/bulk

**Delete several customers of a company**

Deletes the customers listed in `ids` from this company.

## Partial results

- **Partial operation:** the customers that can be deleted are deleted, and the rest keep
  their place in `customers_deletion` with the status that explains why. That is why it
  answers `200` with a body instead of `204`, and why it answers `200` even when no row
  could be deleted.
- **`HAS_INVOICES`:** a customer that has invoices cannot be deleted and comes back with
  that row status.

## What deleting means

- **Semantics:** the same semantics as
  `DELETE /v1/companies/{company_id}/customers/{customer_id}` — the customer is retained
  internally for tax record-keeping purposes but is no longer exposed by the API, its
  identifier is released for reuse, and invoices already issued to it keep their own copy of
  the recipient's details.
- **Deleting is not deactivating:** deleting frees the identifier, so the same NIF can be
  registered again, while `PATCH` with `active: false` leaves the customer where it is with
  its NIF still taken.

### 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.
- **ids** (required) in query `string`: Comma-separated customer IDs

### Responses

#### 200: Request processed. Always `200` when the batch was processed, **even if not a single
customer could be deleted**: the status code says whether the request was understood
and run, not whether every row went well. The per-row split is in the report.


**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** `CustomerBulkDeleteResult`: Report of a bulk delete of customers, in the same shape every batch operation of this API uses (ADR-0011): `metadata` (what the operation was), **one entry per identifier submitted** in its original position and never filtered, and `statistics` derived by counting those entries. **This endpoint deletes, it does not deactivate.** A deleted customer is retained internally for tax record-keeping but is no longer exposed by the API, and **its identifier is released for reuse** (the unique indexes only cover rows that are not deleted). Deactivating is the opposite: the customer stays where it is, marked inactive, with its NIF still taken. A customer that has invoices can never be deleted — it can only be deactivated — which is why it gets its own row status (`HAS_INVOICES`) instead of being reported as a failure. **HTTP status:** always `200` when the request was processed, even if no row could be deleted. The code says whether the request was processed, not whether every row went well; the split is in this report.

#### 400: Bad request. From a dashboard session, a `BeeL-Environment` header whose value is not a
known environment answers `ENVIRONMENT_HEADER_INVALID`, with the header named in
`error.details` (`{"BeeL-Environment": "invalid"}`); it is never read as production.

`BUSINESS_RULE_VIOLATION` is the generic answer to an operation the resource does not
admit in its current state when that state has no code of its own. It carries no
`details`; read the resource again before retrying.


**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": "BAD_REQUEST",
    "message": "Invalid request"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 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")

#### 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

## 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")

## CustomerBulkDeleteResult

Report of a bulk delete of customers, in the same shape every batch operation of this API
uses (ADR-0011): `metadata` (what the operation was), **one entry per identifier submitted**
in its original position and never filtered, and `statistics` derived by counting those
entries.

**This endpoint deletes, it does not deactivate.** A deleted customer is retained internally
for tax record-keeping but is no longer exposed by the API, and **its identifier is released
for reuse** (the unique indexes only cover rows that are not deleted). Deactivating is the
opposite: the customer stays where it is, marked inactive, with its NIF still taken. A
customer that has invoices can never be deleted — it can only be deactivated — which is why
it gets its own row status (`HAS_INVOICES`) instead of being reported as a failure.

**HTTP status:** always `200` when the request was processed, even if no row could be
deleted. The code says whether the request was processed, not whether every row went well;
the split is in this report.

- **metadata** (required) `CustomerBulkDeleteMetadata`: What the operation was, and whether it was a rehearsal.
- **customers_deletion** (required) `array[CustomerBulkDeleteItem]`: One entry per identifier submitted, in the order they were sent and never filtered: a customer that was not deleted keeps its place and its `index`, so the response lines up with the request.
- **statistics** (required) `CustomerBulkDeleteStatistics`: Counters derived by counting the entries of `customers_deletion`, never accumulated while processing. Each row falls into exactly one of `deleted`, `not_found`, `has_invoices`, `has_recurring_invoice` and `errors`, so those five add up to `total_processed`.
- **deleted_ids** (required) `array[UUID]`: IDs of the customers that were deleted.
- **deactivated_ids** (required) `array[UUID]`: **Deprecated alias of `deleted_ids`** — identical content. The name was wrong: this operation deletes (freeing the identifier), it does not deactivate (which keeps it taken). Read `deleted_ids`. It will be removed no earlier than the `Sunset` date announced for the deprecated flat routes.
- **total** (required) `integer`: **Deprecated.** Same as `statistics.total_processed`. (example: 10)
- **successful** (required) `integer`: **Deprecated.** Same as `statistics.deleted`. (example: 8)
- **failed** (required) `integer`: **Deprecated.** Number of submitted identifiers that were NOT deleted, for whatever reason. It lumps together a customer that does not exist and one that has invoices and never can be deleted — which is exactly why the per-row `status` exists. Read `statistics`. (example: 2)
- **errors** `array[CustomerBulkDeleteLegacyError]`: **Deprecated.** Projection of the entries of `customers_deletion` that were not deleted, in the same order. Read `customers_deletion`, which also reports the rows that worked.

## 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"})

## CustomerBulkDeleteMetadata

What the operation was, and whether it was a rehearsal.

- **total_customers** (required) `integer`: Number of identifiers submitted. (example: 10)
- **operation** (required) `string`: The batch operation this report belongs to. — one of: DELETE_CUSTOMERS (example: "DELETE_CUSTOMERS")
- **is_dry_run** (required) `boolean`: Always `false`. Deleting has no rehearsal mode; the field is here so every batch report of this API has the same shape. (example: false)

## CustomerBulkDeleteItem

How one submitted identifier ended up.

- **index** (required) `integer`: Position of the identifier in the submitted list (0-based). (example: 0)
- **customer_id** (required): The identifier that was submitted.
- **status** (required) `string`: Outcome of this row, in the vocabulary of deleting (deleting validates nothing beforehand, so it does not speak of `VALID`): - `DELETED`: deleted; its identifier is free to be used again - `NOT_FOUND`: no such customer, or the current company/grant cannot see it - `HAS_INVOICES`: it has invoices, so it cannot be deleted — **deactivate it instead** (`PATCH` with `active: false`). This is a business rule, not a failure: retrying the delete will never work - `HAS_RECURRING_INVOICE`: an active or paused recurring invoice template uses it as its customer, so it cannot be deleted — finish the template or change its customer first. This is a business rule, not a failure: retrying the delete will never work - `ERROR`: an unexpected failure while processing this row — one of: DELETED, NOT_FOUND, HAS_INVOICES, HAS_RECURRING_INVOICE, ERROR (example: "DELETED")
- **error**: Present only when the customer was not deleted.

## CustomerBulkDeleteStatistics

Counters derived by counting the entries of `customers_deletion`, never accumulated while
processing. Each row falls into exactly one of `deleted`, `not_found`, `has_invoices`,
`has_recurring_invoice` and `errors`, so those five add up to `total_processed`.

- **total_processed** (required) `integer`: Identifiers submitted. (example: 10)
- **deleted** (required) `integer`: Customers deleted (their identifiers are free again). (example: 7)
- **not_found** (required) `integer`: Identifiers that do not exist, or that the current company/grant cannot see. (example: 1)
- **has_invoices** (required) `integer`: Customers that have invoices and therefore cannot be deleted. Not a failure: deactivate them instead. (example: 2)
- **has_recurring_invoice** (required) `integer`: Customers with an active or paused recurring invoice template using them as its customer, and therefore cannot be deleted. Not a failure: finish the template or change its customer instead. (example: 0)
- **errors** (required) `integer`: Rows that failed for an unexpected reason. (example: 0)

## UUID

Universally Unique Identifier (UUID v4)

Type: `string` (uuid)

## CustomerBulkDeleteLegacyError

**Deprecated.** Legacy shape of a row that was not deleted.

- **customer_id** (required) `UUID`: Universally Unique Identifier (UUID v4)
- **error** (required) `CustomerBulkDeleteError`: Why a row was not deleted.

## CustomerBulkDeleteError

Why a row was not deleted.

- **code** (required) `string`: Stable, uppercase, never translated — this is what an integration compares. `NOT_FOUND` means the id does not resolve to a customer of the company, and `INTERNAL_ERROR` that the row failed for an unexpected reason and was not deleted. `CLIENT_HAS_INVOICES` is deliberately the same code the single `DELETE` answers with `409`, so the single and the bulk route never disagree about the same customer. `CLIENT_HAS_RECURRING_INVOICE`: an active or paused recurring invoice uses the customer as its recipient. The single `DELETE` rejects the same case with `400` `REFERENCED_BY_RECURRING_INVOICE`. — one of: NOT_FOUND, CLIENT_HAS_INVOICES, CLIENT_HAS_RECURRING_INVOICE, INTERNAL_ERROR (example: "CLIENT_HAS_INVOICES")
- **message** (required) `string`: Human-readable text, already resolved to the language of the request. (example: "A customer with invoices cannot be deleted: its tax record is kept intact. Deactivate it (active: false) to stop using it.")


---

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