---
title: "Send Assistant Message"
method: POST
path: "/api/v1/assistant-threads/messages"
tags: ["authed"]
---

# Send Assistant Message

`POST /api/v1/assistant-threads/messages`

## Request

- Base URL: `https://api.teaflask.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.teaflask.com/api/v1/assistant-threads/messages`
- Auth: none declared

Create-or-continue the conversation, queue this message's turn,
and wake the thread's ConversationWorkflow (TEA-772: row first, then
the contentless wake). Returns the thread and queued turn
immediately; the answer streams over the thread's SSE stream, and
the run id is the entity's to capture. A message sent while the
previous turn still runs QUEUES — the inbox is the queue, durable
and ordered; the retired busy 409 has no successor. A parked
previous turn is voided at the entity's member open — its pending
work superseded with the outcome recorded as unknown, one beat
after the 201. If the orchestration service can't be
reached the turn is honestly settled failed and the caller gets the
503 — never a message that silently goes nowhere.

## Request body

- SendAssistantMessageRequest
  - `thread_id` string, uuid, nullable
  - `message` string
  - `attachment_ids` string[]

## Response `201`

Successful Response

- SendAssistantMessageResponse
  - `thread` AssistantThreadResponse, required
    - `id` string, uuid, required
    - `title` string, required
    - `busy` boolean, required
    - `stream_thread_id` string, required
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
  - `turn` AssistantTurnResponse, required
    - `id` string, uuid, required
    - `thread_id` string, uuid, required
    - `user_message` string, required
    - `kind` 'delivery', required
    - `attachments` TurnAttachmentResponse[]
      - `id` string, uuid, required: The attachment's id.
      - `kind` 'image' | 'document' | 'video' | 'audio' | 'file', required: The assistant_attachments kind CHECK's closed set.
      - `format` string, nullable, required: The file's format, such as png or pdf; null when unknown.
      - `filename` string, required: The file's name as uploaded.
      - `byte_size` integer, required: The file's size in bytes.
    - `status` 'queued' | 'working' | 'awaiting_input' | 'succeeded' | 'failed' | 'parked' | 'superseded' | 'stopped', required
    - `error` string, nullable, required
    - `failure_reason` TurnFailureReason: Why the exchange failed: what kind of failure it was, and its facts.
      - `kind` 'no_backup_outage' | 'own_key_throttled' | 'own_key_outage' | 'own_key_declined' | 'own_key_required' | 'own_key_verifying' | 'backup_too_small' | 'summary_unavailable' | 'unfunded' | 'pocket_change_cap' | 'empty_balance' | 'model_contract_fault' | 'model_content_refused' | 'model_context_overflow' | 'model_provider_throttled' | 'subscription_unfunded' | 'subscription_unservable' | 'subscription_connect_required' | 'not_configured' | 'worker_lost' | 'runaway' | 'own_key_out_of_credit' | 'own_key_limited' | 'model_unlisted' | 'open_models_unavailable' | 'open_models_switched_off' | 'own_account_blocks_model', required: What kind of failure ended the exchange.
      - `detail` TurnFailureDetail: The facts behind the failure; each kind carries only its own.
        - `model_id` string, nullable: The model that was answering.
        - `provider` 'anthropic' | 'openai' | 'openrouter': Whose API a model call dials — the dimension BYOK keys, probes, bounce classification, and platform-key selection all key on. The vocabulary is append-only and each new value arrives with its integration (probe + classifier + key-shape rule + a platform key unless BYOK-only), never ahead of it.
        - `funding_source` 'balance' | 'byok' | 'end_user_subscription': Which pocket a model call bills against: the org's prepaid balance, the org's BYOK key, or an identified visitor's connected subscription. Per call, not per turn — one turn's rows may mix pockets. Closed partition under the llm_call_usage CHECK; the deduction paths drain the balance only for BALANCE rows. Complimentary money is BALANCE spend.
        - `which` 'message' | 'conversation', nullable: For a request too long for the model: whether the message alone or the whole conversation was too long.
        - `cause` 'tokens' | 'bytes' | 'summary_rejected', nullable: For a request too long for the model: what ran out.
        - `window_tokens` integer, nullable: How many tokens the model can read at once.
        - `plan_lapse` 'quota' | 'auth': Why a connected plan stopped paying: its usage limit, or its sign-in.
        - `balance_empty` boolean, nullable: For a declined API key: true when the balance was empty too, so neither could pay.
        - `welcome_credit_locked` boolean, nullable: For an empty balance: true when the organization had never added a card, so its welcome credit was still locked.
        - `redo_attempted` boolean, nullable: For an assistant that stopped unexpectedly: true when the step was already started again once on a fresh machine before it was given up.
        - `writer_prefix` string, nullable: For an assistant that stopped unexpectedly: the lost step's run and step ids, the prefix every write it made is labelled with — for support to find its trail.
        - `resets_at` string, date-time, nullable: When the key's spending limit resets.
    - `workflow_id` string, required
    - `run_id` string, nullable, required
    - `pending_interrupt_ids` string[]
    - `awaiting_round` integer, nullable
    - `pending_approvals` ApprovalRequestedMarker[]
      - `interrupt_id` string, required
      - `tool_name` string, nullable
      - `tool_args` object
      - `tool_input_schema` object, nullable
      - `tool_output_schema` object, nullable
      - `prompt` string, required
      - `tool_call_id` string, nullable
      - `round` integer
      - `gated` boolean
      - `trust_available` boolean
    - `pending_approval_displays` object
    - `unservable_interrupt_ids` string[]
    - `pending_coworker_executions` CoworkerExecutionRequestResponse[]
      - `ordinal` integer, required
      - `label` string, required
      - `child_session_id` string, required
      - `round` integer, required
      - `expires_at` string, date-time, required
      - `parked` boolean, required
      - `card` ToolExecutionRequestedMarker, required: The tool_execution_requested CUSTOM event's value payload. The tool_call_id anchors the card to the tool-call events already in the log; round is the loop round that paused, as on the approval card.
        - `interrupt_id` string, required
        - `tool_call_id` string, nullable
        - `tool_name` string, required
        - `round` integer
        - `request` ToolExecutionRequestPayload, required: The request half of a tool_execution_requested marker. kind is an open, additive vocabulary — clients ignore kinds they don't recognize. expires_at is advisory (the wait window that parks the turn rules).
          - `kind` string, required
          - `intent` object, nullable
          - `action` object, nullable
          - `expires_at` string, date-time, nullable
    - `pending_coworker_approvals` CoworkerApprovalRequestResponse[]
      - `ordinal` integer, required
      - `label` string, required
      - `child_session_id` string, required
      - `round` integer, required
      - `expires_at` string, date-time, required
      - `parked` boolean, required
      - `card` ApprovalRequestedMarker, required: The approval_requested CUSTOM event's value payload. The tool fields mirror the pending approval's best-effort shape (absent when no tool call was stopped); tool_input_schema / tool_output_schema are the paused tool's registered JSON schemas (a tool view reads the call without the catalog). round is the loop round that paused — what a fallback restart resumes one past. gated: the locked action-approval gate raised this pause; trust_available: the card may offer approve-and-stop-asking, scoped to the asker's own session (org hitl with trust enabled, never the gate).
        - `interrupt_id` string, required
        - `tool_name` string, nullable
        - `tool_args` object
        - `tool_input_schema` object, nullable
        - `tool_output_schema` object, nullable
        - `prompt` string, required
        - `tool_call_id` string, nullable
        - `round` integer
        - `gated` boolean
        - `trust_available` boolean
    - `agent_revision_id` string, uuid, nullable, required
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required

## Other responses

- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not found
- `405`: Method not allowed
- `409`: The assistant's stored configuration cannot run, or the conversation's pending-message queue is at its cap (a resource bound — a message during a live turn still queues)
- `413`: The request body is over the 4 MiB message ceiling
- `422`: Validation error
- `500`: Internal server error
- `503`: Orchestration unavailable

## Changes

- **2026-11-03** `c6ac9fd6657f`: 9 info
  - removed the `STEEP_LEARNING_DISABLED` enum value from the `error/code` response property for the response status `401`
  - removed the `STEEP_LEARNING_DISABLED` enum value from the `error/code` response property for the response status `403`
  - removed the `STEEP_LEARNING_DISABLED` enum value from the `error/code` response property for the response status `404`
  - removed the `STEEP_LEARNING_DISABLED` enum value from the `error/code` response property for the response status `405`
  - …5 more

[Operation history](https://skmtc.dev/teaflask/apis/teaflask-api/changes/api/v1/assistant-threads/messages/post.md)

---

[API](https://skmtc.dev/teaflask/apis/teaflask-api.md) - [All operations](https://skmtc.dev/teaflask/apis/teaflask-api/llms.txt) - [OpenAPI document](https://skmtc.dev/teaflask/apis/teaflask-api/revisions/c6ac9fd6657f?raw)
