:::endpoint POST /v1/admin/tickets/{ticket}/messages

An answer to the trader, or with `internal` a note for the team. The same as
the reply box on the website, under the same rules.

An answer moves the ticket to `awaiting_trader` and restarts the clock on a
quiet ticket. The team's first answer switches the assistant off for good on
this ticket, and takes the ticket when nobody holds it yet: the first reply
takes it. The trader hears of it the way any reply tells them: the bell, a
toast, and an email when they are away and the firm has that mail switched on.

A note touches none of that. Not the status, not the assistant, not who holds
the ticket, and the trader never sees it.

A picture travels as base64 inside JSON, like every upload in this API. Its
type is worked out from the bytes, never from the name: a JPEG, PNG, GIF or
WebP, at most 10 MB once decoded. It is stored re-encoded as WebP, at most 1600
pixels on its longest side.

Needs a firm. Our own staff name one with the `Propper-Workspace` header.

## Request

| Field        | Description |
| ------------ | ----------- |
| `body`       | The text, up to 5000 characters. Required without a picture |
| `image`      | A picture, as `name` (up to 255 characters, what the trader sees it called) and `data` (base64) |
| `internal`   | `true` for a note only the team reads. Defaults to `false` |

```bash
curl -X POST "https://api.proppertrading.com/v1/admin/tickets/tkt_cbqjrnz84cq45ca7/messages" \
  -H "Authorization: Bearer ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f" \
  -H "Propper-Workspace: acme" \
  -H "Idempotency-Key: 4e6a8c0b-2d4f-4b6a-8c1e-3f5a7b9d0c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi Leila, a larger account is a new challenge. This is where you find them.",
    "image": { "name": "challenges.png", "data": "iVBORw0KGgoAAAANSUhEUgAAACgAAAAY..." }
  }'
```

## Response

`201`, with the message as [the conversation](/docs/api/admin/ticket-messages)
shows it:

```json
{
    "data": {
        "object": "ticket_message",
        "id": "tmsg_ua7ypkjx6kas5gvf",
        "ticket_id": "tkt_cbqjrnz84cq45ca7",
        "from": "admin",
        "author": "Ios Tester",
        "body": "Hi Leila, a larger account is a new challenge. This is where you find them.",
        "image_url": "/v1/admin/tickets/tkt_cbqjrnz84cq45ca7/messages/tmsg_ua7ypkjx6kas5gvf/image",
        "image_name": "challenges.png",
        "internal": false,
        "streaming": false,
        "via_email": false,
        "event": null,
        "read_at": null,
        "created_at": "2026-10-15T22:29:42+00:00"
    },
    "meta": {
        "request_id": "req_01m50tyz4fewz8hhjt83b9p60g"
    }
}
```

Retrying with the same `Idempotency-Key` answers the same message and sends
nothing. A second message is a new key.

## Errors

| Status | Code                      | Meaning                                                                         |
| ------ | ------------------------- | ------------------------------------------------------------------------------- |
| 400    | `workspace_required`      | Asked by our own staff without naming a firm                                    |
| 400    | `missing_idempotency_key` | A POST without an `Idempotency-Key` header                                      |
| 400    | `invalid_image`           | `image.data` is not base64, is empty, or is not a JPEG, PNG, GIF or WebP        |
| 400    | `image_too_large`         | The picture is more than 10 MB once decoded                                     |
| 401    | `invalid_token`           | Not a token, or the secret does not match                                       |
| 401    | `revoked_token`           | Signed out here or from another device                                          |
| 401    | `expired_token`           | Ninety days without use, or a set expiry passed                                 |
| 403    | `admin_access_required`   | This account no longer helps run the environment                                |
| 403    | `insufficient_scope`      | This device does not hold `admin:write`                                         |
| 403    | `workspace_not_permitted` | The `Propper-Workspace` header names an environment this account cannot act in |
| 403    | `forbidden`               | Without `view_tickets` and `manage_tickets`                                     |
| 404    | `resource_not_found`      | No ticket with that id in this firm                                             |
| 409    | `ticket_ended`            | The ticket was resolved or closed. A new question becomes a new ticket          |
| 422    | `validation_failed`       | No `body` and no picture, a `body` that is too long, or an `image` without `name` or `data` |