---
title: "Bulk Actions"
description: "Applies one action to many customers at once, for multi-select workflows."
---

`POST /api/customers/bulk`

Applies a single action to a list of customer ids. Each id is processed independently: one
conflicting or missing customer does not abort the rest. The response is always **`200 OK`**,
even when some ids failed, so check `failed` rather than relying on the HTTP status alone.

## Authentication

```bash
BASE_URL="https://uptimeify.io"
TOKEN="<your-api-token>"
```

## Request Body

```json
{
  "ids": [101, 102, 103],
  "action": "reports",
  "payload": {
    "enabled": true
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|--------------|
| `ids` | number[] | yes | Customer ids (the numeric `id`, not `publicId`). Non-empty, at most **200** entries, positive integers, no duplicates. |
| `action` | string | yes | One of `reports`, `activate`, `deactivate`, `delete`, `package`. |
| `payload` | object | depends on `action` | Required for `reports` and `package` (see below). Ignored for `activate`, `deactivate`, `delete`. |

### Actions

| Action | Effect | `payload` |
|--------|--------|-----------|
| `reports` | Sets `monthlyReportsEnabled` on each customer. | `{ "enabled": boolean }` (required) |
| `activate` | Sets the customer's status to `active` (a no-op if already active) and cascades monitor statuses accordingly. | none |
| `deactivate` | Sets the customer's status to `inactive` (a no-op if already inactive) and cascades monitor statuses accordingly. | none |
| `delete` | Hard-deletes the customer. Monitors and check history are removed with it via cascade. **Not reversible.** | none |
| `package` | Reassigns the customer to a different package by id. | `{ "packageId": number }` (required) |

`package` only accepts a numeric `packageId` that belongs to **the target customer's own
organization**. For every caller except a global admin that is the same thing as your own
organization, because you can only address customers inside it. A global admin addressing
customers across organizations needs a `packageId` from each customer's own organization; one
belonging to a different organization is rejected with `invalidPackageType`. Unlike [Update Customer](/api/customers/update-customer), it does not resolve a legacy
`packageType` string or display name, since guessing which package was meant across up to 200
rows at once is not something the bulk endpoint does; look up the id from
[List Package Configs](/api/organization/list-package-configs) first.

`payload.enabled` and `payload.packageId` are validated **once, up front**, before any customer
is touched. A malformed payload fails the whole request with `400 invalidRequestBody`; it never
turns into 200 identical per-id failures.

## Example Request

```bash
curl -X POST "$BASE_URL/api/customers/bulk" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [101, 102, 103],
    "action": "reports",
    "payload": { "enabled": true }
  }'
```

## Example Response (partial result)

```json
{
  "ok": [101, 103],
  "failed": [
    { "id": 102, "reason": "customerNotFound" }
  ]
}
```

`ok` lists the ids that were updated. `failed` lists the ids that were not, each with a `reason`
string. Render the split (for example "2 of 3 updated"), never a blanket success from the fact
that the request itself returned `200`.

## Common Errors

These fail the **whole request** before any customer is touched:

- `401 unauthorized` you are not logged in
- `403 forbidden` your role is `readonly`, or you are a global-supporter (both are read-only for this endpoint)
- `400 invalidRequestBody` `ids` is missing/empty/not an array, contains more than 200 entries, contains a non-integer, a value `<= 0`, or a duplicate; `action` is not one of the five supported actions; or `payload.enabled` / `payload.packageId` is missing or the wrong type for the chosen action

These appear per id, inside `failed[].reason`, without failing the request:

| Reason | Meaning |
|--------|---------|
| `customerNotFound` | The id does not exist, or belongs to another organization (global admins may address any id; everyone else is scoped to their own organization). Deliberately indistinguishable from "does not exist", so a caller cannot use this endpoint to probe which ids exist elsewhere. |
| `invalidPackageType` | `package` action only: `payload.packageId` does not resolve to a package config owned by this customer's organization. |
| `organizationIdRequired` | The caller's session has neither an organization nor global-admin scope. Only possible for a malformed session/token; a normally authenticated caller never sees this. |
| `unknown` | Any other rejection, most commonly a customer-scoped caller (a restricted role, or a customer-scoped API/agent token) addressing an id outside their assigned customers. Also the fallback for any error that carries no stable `data.code`. |

