---
title: Errors and request IDs
slug: errors-and-request-ids
docTags: 
createdAt: 2026-09-25T03:13:45.294Z
---

***

## Errors and request IDs

Every error from the Partner API uses one JSON envelope, whatever produced it (parameter validation, authentication, rate limiting, a business rule, or an unexpected failure):

```json
{
  "error": {
    "message": "Customer can not be enrolled",
    "status": 422,
    "code": "validation_error",
    "request_id": "4f0d8c1e-2b1a-4a5e-9c3d-7e6f5a4b3c2d"
  }
}
```

| Field        | Always present | Meaning                                                                                                                                         |
| ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `message`    | yes            | Human-readable explanation. Wording may change; do not branch on it.                                                                            |
| `status`     | yes            | The HTTP status, repeated in the body.                                                                                                          |
| `code`       | almost always  | Stable machine-readable code. Branch on this. A few legacy business-rule errors return only `message`; treat a missing `code` as `bad_request`. |
| `request_id` | yes            | Correlation id for the request. Also sent as the `X-Request-Id` response header.                                                                |

Some codes add fields:

- `validation_error` adds `errors`, an array of one string per failing parameter.
- `api_error` adds `error_id` (unique to this failure) and `error_signature` (identical for repeats of the same underlying bug).
- `POST /partner/v1/customers/check` adds `details`, a map of field → reason.

## Error codes

| HTTP | `code`                        | When                                                                                                                                                            |
| ---- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `validation_error`            | A parameter is missing, has the wrong type, or fails a documented constraint. `errors` lists each problem.                                                      |
| 400  | `invalid_request_body`        | The body is not valid JSON for the declared `Content-Type`.                                                                                                     |
| 400  | `bad_request` / *(none)*      | A business rule rejected the request, for example `Customer already have active enrollment`.                                                                    |
| 401  | `unauthenticated`             | Missing, malformed, expired or revoked bearer token, or the partner has no API access. Request a new token with `POST /partner/v1/authenticate`.                |
| 402  | `stripe_card_error`           | A card payment failed. `stripe_code` and `stripe_decline_code` carry Stripe's reason.                                                                           |
| 403  | `forbidden`                   | The key is read-only and the request is a write, or the user role does not permit the action.                                                                   |
| 404  | `not_found`                   | No such record for this partner. Also returned for records outside a scoped key's owners or property groups.                                                    |
| 405  | `method_not_allowed`          | The path exists but not for this HTTP method.                                                                                                                   |
| 409  | `invalid_state_transition`    | The record is not in a state that allows this action, for example approving an application that is already approved. Fetch the record and inspect its `status`. |
| 409  | `lock_failed`                 | The record changed while your request was in flight. Fetch it again and retry.                                                                                  |
| 422  | `validation_error` / *(none)* | The request was understood but cannot be processed, for example a customer that fails eligibility.                                                              |
| 429  | `rate_limited`                | Rate limit exceeded. Honour the `Retry-After` header. See [Rate limits](https://rate-limits.md).                                                                |
| 500  | `api_error`                   | Unexpected server error. Quote `request_id` and `error_id` to support.                                                                                          |
| 503  | `request_timeout`             | The request exceeded the server's processing time. Safe to retry GETs; for writes, fetch the record first to see whether the change was applied.                |

Codes are stable: a code is never renamed or removed within `/partner/v1/`. New codes may be added at any time, so treat an unknown code by its HTTP status.

## Request IDs

Every response, success or failure, carries an `X-Request-Id` header. Error bodies repeat it as `request_id`.

- **Send your own.** If you include an `X-Request-Id` header of 1–128 characters from `A–Z a–z 0–9 _ . -`, it is used as-is and echoed back, so you can correlate with your own logs. Anything else is replaced with a generated UUID.
- **Log it.** Store the id with every request you make.
- **Quote it.** Include the `request_id` (and `error_id` for a 500) when contacting [support@boompay.app](mailto\:support@boompay.app). It lets us find the exact request in our logs.

## Retrying

- `GET` requests are safe to retry.
- On `429` wait for `Retry-After` seconds before retrying; see [Rate limits](https://rate-limits.md) for a backoff recipe.
- On `5xx` or a network failure after a write, **read the record back before retrying**. `POST /partner/v1/enrollments/{id}/report_rental_payments` and application decisions have real-world side effects, and the API does not yet accept an idempotency key.
