# Send a thread message

Append a user message and queue its run. Set top-level model to a supported Hoplite model ID, such as gpt-6-sol or claude-sonnet-5, to select the model for this message. Top-level model takes precedence over the legacy metadata.model field; omit both to use the thread's configured default.

`POST /api/threads/{id}/messages`

Required permission: `thread:update`.

## Parameters

| Name | In | Type | Required | Description |

| --- | --- | --- | --- | --- |

| `id` | path | `string` | Yes | The Hoplite thread ID. |

| `Idempotency-Key` | header | `string` | No | Required for writes authenticated by a service credential and for service-account lifecycle writes. Other session writes may opt in. Reuse the key and payload for retries. Settled keys remain reserved for at least seven days; a transient 4xx releases its key. Changed payloads return 409. Match any legacy clientOperationId/clientMessageId in the body. |

## Request body

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `clientMessageId` | `string` | No | min length 1 | — |
| `content` | `string` | Yes | min length 1, pattern \S | — |
| `model` | `string` | No | min length 1, pattern \S | Model ID or connection route for this run. Takes precedence over metadata.model; validated against currently available models. |
| `attachments` | `object[]` | No | max 10 items | Previously uploaded image artifacts to attach to this message. |
| `attachments.artifactId` | `string` | Yes | min length 1 | — |
| `attachments.contentType` | `string` | Yes | min length 1 | — |
| `attachments.name` | `string` | No | min length 1 | — |
| `metadata` | `object` | No | — | Optional message metadata. metadata.model is a supported Hoplite model ID, such as gpt-6-sol or claude-sonnet-5, that selects this message run's model. |
| `metadata.model` | `string` | No | min length 1 | Supported Hoplite model ID for this message run. |
| `metadata.[key]` | `object` | No | — | An additional property with any JSON value. |

```json
{
  "content": "Use the faster model for this follow-up.",
  "metadata": {
    "model": "gpt-6-sol"
  }
}
```

## Example request

```bash
curl --request POST \
  --url https://api.hoplite.sh/api/threads/thr_01JQ7B/messages \
  --header "X-Api-Key: $HOPLITE_API_KEY" \
  --header "Idempotency-Key: $HOPLITE_OPERATION_ID" \
  --header 'Content-Type: application/json' \
  --data '{
  "content": "Use the faster model for this follow-up.",
  "metadata": {
    "model": "gpt-6-sol"
  }
}'
```

## 201 response

The message was accepted and its run was queued.

Content type: `application/json`. Response type: `AppendThreadMessageResponse`.

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `ok` | `boolean` | Yes | constant true | — |
| `message` | `object` | Yes | — | — |
| `message.id` | `string` | Yes | — | — |
| `idempotent` | `boolean` | No | — | — |
| `queued` | `boolean` | No | — | — |
| `run` | `RunSummary \| null` | No | — | — |
| `run.Option 1` | `RunSummary` | No | — | — |
| `run.Option 1.id` | `string` | Yes | — | — |
| `run.Option 1.status` | `string` | No | — | — |
| `run.Option 2` | `null` | No | — | — |

```json
{
  "ok": true,
  "message": {
    "id": "msg_01JQ7D"
  },
  "queued": true
}
```

## Response headers

| Header | Type | Statuses | Description |

| --- | --- | --- | --- |

| `RateLimit` | `string` | `201`, `400`, `401`, `402`, `403`, `429`, `500`, `501`, `503` | Current ingress policy, remaining requests, and seconds until reset, for example `"ingress";r=599;t=60`. |

| `RateLimit-Policy` | `string` | `201`, `400`, `401`, `402`, `403`, `429`, `500`, `501`, `503` | Ingress quota and window, for example `"ingress";q=600;qu="requests";w=60`. |

| `X-RateLimit-Limit` | `integer` | `201`, `400`, `401`, `402`, `403`, `429`, `500`, `501`, `503` | Maximum requests allowed in the current rolling window. |

| `X-RateLimit-Remaining` | `integer` | `201`, `400`, `401`, `402`, `403`, `429`, `500`, `501`, `503` | Requests remaining in the current rolling window. |

| `X-RateLimit-Reset` | `integer` | `201`, `400`, `401`, `402`, `403`, `429`, `500`, `501`, `503` | Unix timestamp when the current rolling window resets. |

| `x-request-id` | `string` | `201`, `400`, `401`, `402`, `403`, `429`, `500`, `501`, `503` | Request correlation ID. |

| `Retry-After` | `integer` | `429`, `503` | Seconds until another request should be attempted. |

| `X-Retry-After` | `integer` | `429`, `503` | Legacy retry delay in seconds. |

## Status codes

- `201` — The message was accepted and its run was queued.

- `400` — The message or requested model is invalid.

- `401` — The API key is missing, invalid, or lacks the operation's permission.

- `402` — The organization does not have access to the requested product capability.

- `403` — The key's current user no longer has the required organization membership or role.

- `429` — The API-key rate limit has been exceeded.

- `500` — The API encountered an unexpected failure.

- `501` — The deployment has not configured the requested capability.

- `503` — A required API dependency, including the rate limiter, is unavailable.