---
title: "Delete several customers of a company"
method: DELETE
path: "/v1/companies/{company_id}/customers/bulk"
tags: ["CompanyCustomers"]
---

# Delete several customers of a company

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

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/companies/{company_id}/customers/bulk`
- Auth: HTTP bearer

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.

## Query parameters

- `ids` string, required

## Response `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.

- object: 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` boolean, required
  - `data` object, required: 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` CustomerBulkDeleteMetadata, required: What the operation was, and whether it was a rehearsal.
      - `total_customers` integer, required: Number of identifiers submitted.
      - `operation` 'DELETE_CUSTOMERS', required: The batch operation this report belongs to.
      - `is_dry_run` boolean, required: Always `false`. Deleting has no rehearsal mode; the field is here so every batch report of this API has the same shape.
    - `customers_deletion` CustomerBulkDeleteItem[], required: 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.
      - `index` integer, required: Position of the identifier in the submitted list (0-based).
      - `customer_id` string, uuid, required: Universally Unique Identifier (UUID v4)
      - `status` 'DELETED' | 'NOT_FOUND' | 'HAS_INVOICES' | 'HAS_RECURRING_INVOICE' | 'ERROR', required: 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
      - `error` CustomerBulkDeleteError: Why a row was not deleted.
        - `code` 'NOT_FOUND' | 'CLIENT_HAS_INVOICES' | 'CLIENT_HAS_RECURRING_INVOICE' | 'INTERNAL_ERROR', required: 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`.
        - `message` string, required: Human-readable text, already resolved to the language of the request.
    - `statistics` CustomerBulkDeleteStatistics, required: 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` integer, required: Identifiers submitted.
      - `deleted` integer, required: Customers deleted (their identifiers are free again).
      - `not_found` integer, required: Identifiers that do not exist, or that the current company/grant cannot see.
      - `has_invoices` integer, required: Customers that have invoices and therefore cannot be deleted. Not a failure: deactivate them instead.
      - `has_recurring_invoice` integer, required: 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.
      - `errors` integer, required: Rows that failed for an unexpected reason.
    - `deleted_ids` UUID[], required: IDs of the customers that were deleted.
    - `deactivated_ids` UUID[], required: **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` integer, required: **Deprecated.** Same as `statistics.total_processed`.
    - `successful` integer, required: **Deprecated.** Same as `statistics.deleted`.
    - `failed` integer, required: **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`.
    - `errors` 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.
      - `customer_id` string, uuid, required: Universally Unique Identifier (UUID v4)
      - `error` CustomerBulkDeleteError, required: Why a row was not deleted.
        - `code` 'NOT_FOUND' | 'CLIENT_HAS_INVOICES' | 'CLIENT_HAS_RECURRING_INVOICE' | 'INTERNAL_ERROR', required: 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`.
        - `message` string, required: Human-readable text, already resolved to the language of the request.
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `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.
- `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.
- `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`.
- `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.
- `500`: Internal server error
- `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.

---

[API](https://skmtc.dev/beel/apis/beel-public-api.md) · [All operations](https://skmtc.dev/beel/apis/beel-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/beel/apis/beel-public-api/revisions/ffbc0ae31f79?raw)
