---
title: "Add a message to a conversation"
method: POST
path: "/v1/projects/{project_id}/conversations/{conversation_id}/messages"
tags: ["Conversations"]
---

# Add a message to a conversation

`POST /v1/projects/{project_id}/conversations/{conversation_id}/messages`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/conversations/{conversation_id}/messages`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Creates a document from the message text and attaches it to the conversation at the given position. If position is omitted, it is appended at the end.

## Path parameters

- `conversation_id` string, required

## Request body

- object
  - `message` string, required: Message text content to add to the conversation
  - `role` 'user' | 'assistant', required: Role of the message sender
  - `actor_id` string, nullable: Optional actor ID to associate with this message (user identity)
  - `position` integer: Zero-based position. Defaults to MAX+1 (append).
  - `metadata` NullableMetadataBag, nullable: A `MetadataBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.

## Response `201`

Message added

- ConversationMessageRecord
  - `document_id` string: Document ID
  - `role` 'user' | 'assistant' | 'system': Role of the message sender
  - `actor_id` string, nullable: Optional actor ID associated with this message
  - `agent_id` string, nullable: Optional agent ID that generated this message (set for assistant messages produced by generate)
  - `position` integer: Zero-based position in the conversation
  - `metadata` MetadataBag: Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase. No key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.
  - `content` string, nullable: Full text content of the message
  - `idempotency_key` string, nullable: The idempotency key the message was posted with, null when none was sent. A channel that posts each inbound message under its provider's message id finds the transcript row for that message by this key.

## Other responses

- `400`: Invalid request body
- `401`: Unauthorized
- `403`: Forbidden
- `404`: Conversation or actor not found

---

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