---
title: Errors
description: Every status and code the API returns, what each means, and what to do about it.
icon: triangle-alert
sidebar:
  order: 4
---

A failed request returns a JSON body with a single `error` field holding a
snake_case code. Codes are stable and safe to branch on; the wording of any
prose around them is not.

```json
{ "error": "missing_scope" }
```

Some codes carry a detail after a colon, such as `hire_unavailable:10` or
`wrong_id_type:expected_products_got_productVariants`. Split on the first colon
and treat the left side as the code.

## Statuses

| Prop | Type | Default | Description |
| - | - | - | - |
| `200?` | `OK` | - | The call succeeded. Bulk writes return 200 when every row was an update or errored. |
| `201?` | `Created` | - | A bulk write created at least one row. |
| `400?` | `Bad request` | - | The envelope was wrong before any authentication ran, such as a body that is not an array where one was required. |
| `401?` | `Unauthorized` | - | No key, or a key that is unknown, revoked or expired. |
| `403?` | `Forbidden` | - | The key is valid but lacks the scope, or the tenant has no API access. |
| `404?` | `Not found` | - | The addressed resource does not exist on this tenant. |
| `409?` | `Conflict` | - | A delete would cascade past the resource in the URL. Retry with force=true if that is what you want. |
| `413?` | `Payload too large` | - | The request body exceeded the size limit. |
| `422?` | `Unprocessable` | - | The body parsed but failed validation, including malformed ids and unknown field names. |
| `429?` | `Too many requests` | - | The per-minute rate limit or the monthly cap was exceeded. |
| `500?` | `Server error` | - | Something failed on our side. The response carries internal_error and the detail is in our logs. |

## Authentication and limits

| Code | Meaning | What to do |
| --- | --- | --- |
| `missing_authorization` | No `Authorization` header. | Send `Authorization: Bearer bloomcount_…`. |
| `invalid_api_key` | The key is not recognised. | Check the key was copied whole. |
| `revoked` | The key was revoked in the app. | Issue a new key. |
| `expired` | The key passed its expiry date. | Issue a new key. |
| `api_access_revoked` | The tenant's plan no longer includes API access. | Speak to the account owner. |
| `missing_scope` | The key lacks the scope this endpoint needs. | Add the scope in Settings → API, or use a key that has it. |
| `rate_limited` | More than 60 requests in a minute on this key. | Back off and retry. |
| `monthly_limit_exceeded` | The tenant's monthly call cap is spent. | Wait for the reset or upgrade the plan. |

## Request shape

| Code | Meaning | What to do |
| --- | --- | --- |
| `invalid_json` | The body is not valid JSON. | Check the serialiser and the content type. |
| `invalid_body` | The body parsed but is not the shape the endpoint takes. | Compare against the endpoint page. |
| `items_must_be_array` / `groups_must_be_array` | A bulk endpoint was sent something other than an array. | Wrap the rows in an array. |
| `too_many_items` | More than 500 rows in one call. | Split the batch. |
| `payload_too_large` | The body exceeded the size limit. | Send fewer rows, or smaller ones. |
| `invalid_id` / `invalid_event_id` / `invalid_id_format` | An id in the path or body is not a prefixed id. | Send the id exactly as the API returned it. |
| `wrong_id_type:expected_<table>_got_<table>` | A valid id of the wrong kind. | Check which prefix the field wants. |
| `unknown_fields:<names>` | `fields` named paths that do not exist. | Fix the names; the response lists every one that failed. |

## Rows in a bulk write

A bulk endpoint does not fail the whole call for one bad row. It returns the
successful rows in `results` and the rest in `errors`, each carrying the row's
`index` and one of these codes.

### References

| Code | Meaning |
| --- | --- |
| `product_not_found`, `variant_not_found`, `supplier_not_found` | The referenced row is not on this tenant, or does not exist. |
| `price_list_not_found`, `tax_group_not_found`, `event_not_found` | As above, for the named resource. |
| `group_not_found`, `item_not_found` | The group or line is not on the event in the URL. |
| `variant_not_on_product` | The variant exists but belongs to a different product. |
| `supplier_not_on_variant` | The supplier does not sell that variant. |
| `supplier_archived` | The supplier is archived and cannot take new lines. |
| `supplier_default_not_allowed` | The hidden Default supplier cannot be assigned through the API. |
| `move_target_not_found`, `move_target_is_removed_group` | `moveItemsTo` points at a group that is missing, or at one the same call removes. |

### Missing or invalid values

| Code | Meaning |
| --- | --- |
| `missing_required_fields_on_create` | A create is missing a field that only creates require. |
| `name_required`, `price_required` | The named field was empty. |
| `variant_id_required`, `item_id_required`, `group_id_required`, `supplier_id_required` | The row omitted an id the operation needs. |
| `variantId_required_for_variant_patch` | Variant fields were supplied without a `variantId`. |
| `invalid_quantity`, `invalid_group_quantity` | A quantity was not a positive whole number. |
| `supplierMinPacks_supplier_mismatch` | A minimum-order entry names a supplier that is not on the variant. |
| `duplicate` | The same row appears twice in one batch. |

### Hire

| Code | Meaning |
| --- | --- |
| `hire_unavailable:<n>` | Fewer units are free across those dates than the row asked for. The number is what is actually available. |
| `hire_dates_required` | A hire line needs `hireStart` and `hireEnd`. |
| `hire_end_before_start` | The hire window ends before it begins. |
| `not_a_hire_line` | Hire dates were sent for a consumable line. |
| `variant_not_hireable` | The variant is not marked hireable. |
| `hireable_requires_material_product` | Only material products can be hireable. |
| `supplier_not_allowed_on_hire` | Hire lines come from your own stock, so they take no supplier. |

### Reference data

| Code | Meaning |
| --- | --- |
| `color_not_found`, `category_not_found`, `venue_not_found`, `staff_not_found` | No such row on this tenant. |
| `color_name_required`, `category_name_required`, `venue_name_required`, `staff_name_required`, `supplier_name_required` | The name was empty. |
| `color_name_taken`, `category_name_taken`, `supplier_name_taken` | Another row already uses that name. |
| `invalid_hex_format` | A colour's `hex` is not a valid hex colour. |
| `preset_color_immutable`, `preset_category_immutable` | Built-in colours and categories cannot be edited or deleted. |
| `default_supplier_immutable` | The hidden Default supplier cannot be changed. |
| `staff_has_account` | That staff member has a user account, so remove them from Settings instead. |

## Deleting something in use

A delete whose cascade would reach past the resource in the URL returns `409`
with a count, rather than quietly taking the rest with it.

| Code | Meaning |
| --- | --- |
| `product_on_event_items:<n>` | That many event lines use the product. |
| `variant_on_event_items:<n>` | That many event lines use the variant. |
| `price_list_in_use:<n>` | That many events point at the price list. |
| `tax_group_in_use:<n>` | That many price lists use the tax group. |

Adding `?force=true` performs the same delete the app performs, cascade
included, and the response reports what it took. Events have no `force`: their
groups, lines and versions belong to them and go with them.

## Webhooks

| Code | Meaning |
| --- | --- |
| `url_invalid` | The URL did not parse. |
| `url_must_be_https` | Only `https://` endpoints are accepted. |
| `url_credentials_not_allowed` | The URL carried a username or password. |
| `url_port_not_allowed` | Only port 443 is accepted. |
| `url_host_not_allowed` | The host is loopback, private, link-local or a metadata address. |
| `at_least_one_event_required` | A subscription must name at least one event type. |
| `subscription_not_found` | No such subscription on this tenant. |
