> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trellis.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Durable Turn

> Submit a detached chat turn for background-safe processing

Starts a durable turn and returns immediately with `202 Accepted`. The agent continues server-side after the request ends; poll [Get Turn](/api-reference/chats/get-turn) until `status` is terminal.

```http theme={"theme":{"light":"github-light","dark":"github-dark"}}
POST /v1/agents/{agent_id}/chats/{chat_id}/turns
```

<ParamField header="Authorization" type="string" required>
  Bearer token obtained from [Authenticate](/api-reference/authentication/authenticate).
</ParamField>

<ParamField path="agent_id" type="string" required>
  Published agent UUID. Use the chat's `agent_id`; for a legacy null binding, use your organization's configured published agent UUID rather than inferring it from display name or `is_trellis_default`.
</ParamField>

<ParamField path="chat_id" type="string" required>
  Existing member-owned chat UUID. There is no lazy chat creation.
</ParamField>

## Request body

<ParamField body="client_request_id" type="string" required>
  Idempotency key, 1–128 characters. Retrying the same semantic payload returns the original turn. Reusing the ID for a different payload returns `409 IDEMPOTENCY_CONFLICT`.
</ParamField>

<ParamField body="message" type="string" required>
  Non-empty user message.
</ParamField>

<ParamField body="integration_id" type="string">
  Database integration for this and later turns. A valid change persists on the chat. A malformed, unknown, or out-of-organization ID returns `400 Integration not found`; see the idempotency recovery note below.
</ParamField>

<ParamField body="model" type="string">
  Model override for this turn. Populate choices from [List Models](/api-reference/models).
</ParamField>

<ParamField body="upload_ids" type="string[]">
  Upload IDs from [Upload Files](/api-reference/chats/upload-files).
</ParamField>

<ParamField body="saved_query_id" type="string">
  Execute a stored query before agent generation. A database integration must be bound.
</ParamField>

<ParamField body="parameters" type="object">
  Saved-query parameters with string values.
</ParamField>

<ParamField body="scoped_project_ids" type="string[]">
  Project scope to persist. Omit the field to retain the stored scope; send `[]` to clear it. In request order, the server keeps the first occurrence of each valid, organization-owned, permitted project and persists at most the first two surviving IDs. Duplicate, malformed, unknown, out-of-organization, non-permitted, and additional IDs are silently discarded.
</ParamField>

<ParamField body="project_id" type="string">
  Legacy shorthand for a one-element `scoped_project_ids`. Ignored when the array field is present.
</ParamField>

### Compatibility-only fields

The request model accepts these fields for parity with internal/legacy transports. New external clients should avoid them:

| Field             | Type      | Behavior                                                                                  |
| ----------------- | --------- | ----------------------------------------------------------------------------------------- |
| `connection_id`   | string    | Legacy integration alias unless `connection_type` is `"project"`; prefer `integration_id` |
| `connection_type` | string    | Legacy datasource discriminator                                                           |
| `attached_files`  | object\[] | Internal project-file references; external clients should use `upload_ids`                |
| `open_schedule`   | object    | Internal schedule handoff that replaces normal generation                                 |
| `title`           | string    | Does not retitle the required existing chat                                               |
| `voice_mode`      | boolean   | Ignored and forced to `false` on the external REST route                                  |

## Response

<ResponseField name="turn_id" type="string" required>
  Durable turn UUID.
</ResponseField>

<ResponseField name="chat_id" type="string" required>
  Owning chat UUID.
</ResponseField>

<ResponseField name="user_message_id" type="string | null" required>
  Persisted user-message UUID.
</ResponseField>

<ResponseField name="status" type="string" required>
  `in_progress` for a newly accepted turn. An idempotent replay can return the original turn's current status: `in_progress`, `completed`, `failed`, or `cancelled`.
</ResponseField>

<ResponseField name="created_at" type="string" required>
  ISO 8601 acceptance timestamp.
</ResponseField>

## Idempotency and concurrency

* Same `client_request_id` and same semantic payload: returns the original turn; no second user message is written.
* Same ID and different payload: `409` with `detail.code: "IDEMPOTENCY_CONFLICT"`.
* Another running turn on the chat: `409` with `detail.code: "TURN_ALREADY_RUNNING"`.

Generate a new request ID whenever any generation input changes, including the message, model, integration, scope, uploads, saved query, or parameters.

<Warning>
  Datasource binding happens after the durable turn is accepted and its user message and idempotency record are persisted. If `integration_id` is invalid, the server terminalizes that turn and returns `400`, so the supplied `client_request_id` has already been consumed by the failed attempt. Retrying the unchanged request returns the original failed turn; correcting `integration_id` under the same key returns `409 IDEMPOTENCY_CONFLICT`. Correct the integration and submit with a fresh `client_request_id`.
</Warning>

This route shares a single per-member budget with WebSocket `send_message` and `edit_message`: **10 submissions per minute and 3 per 10 seconds combined**. A `429` response includes `Retry-After`; re-authentication does not reset the budget.

<RequestExample>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST \
    "https://api.trellis.sh/v1/agents/AGENT_ID/chats/9f0ac8f2-7a22-4ec8-a778-b0b1b83a86bb/turns" \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "client_request_id": "mobile-20260808-7f4f1fca",
      "message": "How many orders were placed last month?",
      "integration_id": "INTEGRATION_ID",
      "model": "claude-sonnet-5",
      "scoped_project_ids": []
    }'
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  response = requests.post(
      "https://api.trellis.sh/v1/agents/AGENT_ID/chats/9f0ac8f2-7a22-4ec8-a778-b0b1b83a86bb/turns",
      headers={"Authorization": "Bearer YOUR_TOKEN"},
      json={
          "client_request_id": "mobile-20260808-7f4f1fca",
          "message": "How many orders were placed last month?",
          "integration_id": "INTEGRATION_ID",
          "model": "claude-sonnet-5",
          "scoped_project_ids": [],
      },
  )
  response.raise_for_status()
  accepted = response.json()
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const response = await fetch(
    "https://api.trellis.sh/v1/agents/AGENT_ID/chats/9f0ac8f2-7a22-4ec8-a778-b0b1b83a86bb/turns",
    {
      method: "POST",
      headers: {
        Authorization: "Bearer YOUR_TOKEN",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        client_request_id: "mobile-20260808-7f4f1fca",
        message: "How many orders were placed last month?",
        integration_id: "INTEGRATION_ID",
        model: "claude-sonnet-5",
        scoped_project_ids: [],
      }),
    },
  );
  const accepted = await response.json();
  ```
</RequestExample>

<ResponseExample>
  ```json 202 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "turn_id": "26e53cf2-b12d-4a2a-a3b7-5382a87f8f5b",
    "chat_id": "9f0ac8f2-7a22-4ec8-a778-b0b1b83a86bb",
    "user_message_id": "b3ebae7f-e265-47e4-92dd-344d396f0c33",
    "status": "in_progress",
    "created_at": "2026-08-08T18:00:01.234567+00:00"
  }
  ```

  ```json 409 Idempotency conflict theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"detail":{"code":"IDEMPOTENCY_CONFLICT"}}
  ```

  ```json 409 Another turn is running theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"detail":{"code":"TURN_ALREADY_RUNNING"}}
  ```

  ```json 400 Invalid integration theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"detail":"Integration not found"}
  ```

  ```json 404 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"detail":"Chat not found"}
  ```

  ```json 429 theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"code":"RATE_LIMIT","retry_after_seconds":7}
  ```
</ResponseExample>
