Skip to content

Bulk delete labels ​

POST/api/v2/workspaces/{slug}/projects/{project_id}/labels/bulk-delete/

Delete up to 50 labels in one request. Each row goes through the same path as a single Delete a label, so delete guards still apply per row.

200 does not mean every row succeeded

By default this endpoint reports partial success: it answers 200 whenever the batch was processed, even if some rows failed. Read failed and the per-row results rather than branching on the status code alone.

If you want all-or-nothing instead, send all_or_none: true and treat 409 as the failure signal.

Path Parameters ​

slug:requiredstring

The workspace slug. It appears in your Plane URLs — in https://app.plane.so/my-team/projects/, the slug is my-team.

project_id:requiredstring (uuid)

The project the labels belong to. Accepts the project UUID or its bare identifier, for example ENG.

Body Parameters ​

ids:requiredarray of string (uuid)

The ids of the labels to delete. Between 1 and 50 per call. A value that isn't a UUID is a 400 for the whole envelope, not a failed row.

all_or_none:optionalboolean

Defaults to false. Leave it off for partial success: each row runs in its own savepoint, successful rows commit, and you read the breakdown.

Set it to true to make the batch atomic. Every row is still evaluated, but if any row fails the whole batch is discarded and the call answers 409 instead of the 200 envelope. Side effects scheduled by the rows follow the same outcome.

Scopes ​

projects.labels:write

Errors ​

StatusCodeCause
400invalid_requestThe envelope itself is malformed — ids missing or empty, more than 50 rows, or a non-UUID id.
401unauthorizedMissing or invalid credentials.
403forbiddenYour role or token scope can't write labels in this project.
404not_foundNo such workspace or project, or it's outside your tenant.
406not_acceptableThe Accept header asks for a representation the API can't produce.
409conflictAn all_or_none: true batch had at least one failing row, so the whole batch was discarded.
413payload_too_largeThe request body is over the size limit.
415unsupported_media_typeThe body wasn't JSON. These routes are JSON-only — a form or multipart body can't express ids.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Bulk delete labels
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/labels/bulk-delete/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "ids": [
    "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53",
    "9c1f0b3d-6d2e-4c9a-8b41-2f7e5a0d6c88"
  ]
}'
Response200
json
{
  "results": [
    {
      "index": 0,
      "result": "deleted",
      "id": "b7e42a19-3c5d-4f80-9a26-8d1c0f4e7b53"
    },
    {
      "index": 1,
      "result": "failed",
      "type": "not_found",
      "code": "not_found",
      "detail": "No label matches the given query."
    }
  ],
  "succeeded": 1,
  "failed": 1
}
Response409
json
{
  "type": "conflict",
  "code": "conflict",
  "detail": "No rows were written because all_or_none was set and at least one row failed.",
  "errors": [
    {
      "field": "ids.1",
      "code": "not_found",
      "message": "No label matches the given query."
    }
  ]
}

Reading the response ​

The 200 envelope has three members:

  • results — one row per input row, in request order, each carrying its index
  • succeeded / failed — counts, so you can branch without walking the array

A succeeded row is { index, result, id } where result is deleted. A failed row keeps the same members as a top-level error body — type, code, detail and, on validation failures, errors[] — plus index and result: "failed". So the same error-handling code works per row as at the top level. See Errors.

Atomic batches ​

With all_or_none: true, every row is still evaluated — you get the full picture of what would have failed, not just the first problem — and then the batch is rolled back. The 409 body's errors[] entries are prefixed with the row index, as ids.<index>, so you can map each complaint back to the row that caused it.

Because the rollback covers the whole batch, side effects scheduled on commit — activity feed entries, webhooks — are discarded with it.

  • 50 rows per call, and at least 1. Exceeding the cap is a 400 for the whole envelope.
  • JSON only. A form or multipart body can't express ids, so these routes reject it with 415 rather than silently misreading the payload.
  • Reconciling on external ids is not bulk's job. Use Upsert a label per row, or list and diff.
  • ?fields= does not apply here — the response is a per-row result envelope, not a label body. See Sparse fields.