# `bad_request`: Malformed request

> HTTP 400. Something in the request itself is wrong: a query parameter, a header or the body. Change the request and send it again.

The API could not accept the request as written. The problem is always in what you sent (the query string, the `Idempotency-Key` header, or the JSON body of a write), never in the state of the thing you addressed, so the fix is always a change to the request.

The envelope tells you what to change. `param` names the one parameter at fault when there is one, spelled exactly as it should be sent. When that parameter accepts a finite set, `allowed_values` lists the whole set, and `message` repeats it in prose. For an unknown parameter name, `allowed_values` holds the parameter **names** the operation accepts rather than values. Errors in a request body carry no `param`; the message names the property instead.

An unsupported `Commune-Version` header has its own code, `invalid_version`, because the fix is different.

## Why it happens, and how to fix it

### 1. A query parameter the operation does not have

You sent a query parameter the operation does not declare, usually a typo or a name from another API (`?state=` for `?status=`, `?page=` for `?cursor=`, `?per_page=` for `?limit=`). It is refused rather than ignored, because a filter that silently did nothing would look like a filter that matched everything.

**Fix:** Rename it to one of the names in `allowed_values`, or drop it. `param` echoes the name you sent.

### 2. A value outside the parameter's set

The parameter exists, but the value is not one it accepts: a `status` that is not an article status, an `expand` path the operation cannot expand, a `fields` name the resource does not have, or a boolean parameter given something other than `true` or `false`. For comma separated parameters such as `expand` and `fields`, the first value that is not in the set is the one reported.

**Fix:** Pick a value from `allowed_values`. Values are case sensitive and are sent as they appear there.

### 3. A value of the wrong shape

The value is not a set member, it is a number, an identifier or a string, and it failed that check. `limit` has to be a whole number between 1 and 100. An identifier parameter has to be a UUID. Some strings have a maximum length. A comma separated parameter needs at least one value. And a parameter may only be given once, unless it is a repeatable filter such as `status`.

**Fix:** Follow the constraint the message states (for example "Give a whole number between 1 and 100.") and send the parameter once.

### 4. A cursor that cannot be read

`cursor` is opaque and only valid for the operation and the filters that produced it. It is refused when it was edited, truncated or URL decoded twice, when it came from a different operation, or when you changed a filter (`status`, `expand`, `limit` and so on) between pages.

**Fix:** Pass `pagination.next_cursor` from the previous response back unchanged, with exactly the same query parameters. If the filters have to change, start again from the first page without a `cursor`. Do not store cursors to resume later.

### 5. A required parameter is missing

Three parameters are required in some situations. `metric` on `GET /newsletters/{newsletter}/timeseries` has no default. `q` on `GET /search` has to be present (a query shorter than two characters answers with an empty page, not an error). And `newsletter` on `GET /delivery-attempts/{attempt}` and `POST /delivery-attempts/{attempt}/replay` may only be left out by a credential that reaches exactly one newsletter.

**Fix:** Send the parameter the message names. For `metric`, pick one of `allowed_values`. For `newsletter`, send the newsletter's `id` or `handle`; `GET /newsletters` lists the ones your credential reaches.

### 6. A write without a usable Idempotency-Key

Every operation that changes something (every `POST`, `PATCH` and `DELETE`) requires an `Idempotency-Key` header, and refuses the request without one. A key is also refused when it is longer than 255 characters or contains spaces or characters outside printable ASCII.

**Fix:** Send `Idempotency-Key: <value>` with a fresh value, a UUID being the usual choice, for each change you intend to make. When you retry that change, send the same value again so Commune replays the first answer instead of making the change twice.

### 7. A request body the operation cannot read

On a write, the body was not valid JSON, was not a JSON object, named a property the operation does not write (such as `status` on an article, which is moved by the send and schedule operations), gave a property the wrong type or a value that is too long, or failed a rule of its own. The common ones: a `slug` that is not lowercase letters, digits and single hyphens (at most 60 characters); a `PATCH` with no properties in it; `content_markdown` that does not parse (the message gives the line, and HTML is not accepted); a tag `name` that is empty or over 60 characters; a `subscribers` list that is empty, holds more than 500 ids, or holds something that is not a subscriber id; a test send `to` list that is empty, has more than 5 addresses, or holds something that is not an email address; a `scheduled_for` that is missing or is not an RFC 3339 timestamp; and `acknowledge_broken_images` that is not a boolean.

**Fix:** Send `Content-Type: application/json` with a JSON object that follows the rule the message states. The message names the property, the limit and, for Markdown, the line. Nothing was written, so you can send the corrected body with the same `Idempotency-Key`.

### 8. A time window that cannot be read

On the operations that take `since` and `until`, a value was neither a calendar date nor an RFC 3339 timestamp, or `since` was later than `until`.

**Fix:** Send dates such as `2026-08-01` (read as midnight UTC) or timestamps such as `2026-08-01T09:30:00Z`, with `since` earlier than `until`. Leave `until` out to run the window to now.

## Retrying

No: the same request fails the same way, so change what `param` or the message names and send it again (a write that failed this way made no change, so the same `Idempotency-Key` can be reused).

## Example response

```json
{
  "error": {
    "code": "bad_request",
    "message": "Unsupported value \"published\" for query parameter \"status\". Allowed values: draft, scheduled, sending, sent, failed, archived.",
    "param": "status",
    "allowed_values": [
      "draft",
      "scheduled",
      "sending",
      "sent",
      "failed",
      "archived"
    ],
    "request_id": "req_01j9c8h1q7m3n4p5r6s7t8u9v0",
    "docs_url": "https://usecommune.dev/errors/bad_request"
  }
}
```

Often confused with: [`invalid_version`](https://usecommune.dev/errors/invalid_version.md), [`unprocessable`](https://usecommune.dev/errors/unprocessable.md), [`insufficient_scope`](https://usecommune.dev/errors/insufficient_scope.md).
