openapi: 3.0.3
info:
  title: naturali.ai — Sessions API
  version: 1.0.0
  description: >
    Durable, resumable sessions — the single runtime contract (API.md §5,
    A10). Open a session against an agent, append messages, and generate the
    agent's response; the conversation lives in the backing runtime session, keyed
    by the session id, so a session is resumable across requests and processes.
    Sessions front the backing runtime session: the generation cost is
    metered on the runtime and surfaced through
    GET /v1/projects/{project_id}/usage, so the only
    naturali-side record is the ownership mapping (which project and agent the
    session belongs to).
  contact:
    name: naturali.ai
    url: https://naturali.ai
servers:
  - url: '{baseUrl}'
    description: Host of your naturali.ai deployment; every path carries the /v1 prefix.
    variables:
      baseUrl:
        description: Base host URL.
        default: https://api.naturali.ai
tags:
  - name: Sessions
    description: Open durable sessions against an agent and run the conversation.
security:
  - bearerAuth: []
paths:
  /v1/projects/{project_id}/agents/{agent_id}/sessions:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/AgentId'
    post:
      tags: [Sessions]
      summary: Open a session
      description: >
        Opens a durable session against the agent. The session accumulates
        messages and is resumable by id; its lifecycle (open / closed /
        expired) and configuration live in the backing runtime session.
      operationId: createSession
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionCreate'
      responses:
        '201':
          description: The session was opened.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/agents/{agent_id}/sessions/{session_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/AgentId'
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [Sessions]
      summary: Get a session
      description: >
        Returns the session's current state — status, activity timestamps and
        configuration — read live from the backing runtime session, so a resumed
        session reflects everything that has happened since it was opened.
      operationId: getSession
      responses:
        '200':
          description: The session.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/agents/{agent_id}/sessions/{session_id}/messages:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/AgentId'
      - $ref: '#/components/parameters/SessionId'
    post:
      tags: [Sessions]
      summary: Add a message
      description: >
        Appends a user message to the session — plain `message` text or a
        `document_id`, exactly one of the two. `idempotency_key` makes the
        append safe to retry: a repeat with the same key returns the original
        message (HTTP 200) and triggers no new work. When the session has
        `auto_generate` on, the response is the agent's reply (a Generation
        shape) instead of the saved message.
      operationId: addSessionMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionMessageCreate'
      responses:
        '200':
          description: A duplicate append (idempotency key matched); the saved message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionMessage'
        '201':
          description: >
            The message was appended. The saved message, or (when
            `auto_generate` is on) the agent's generated reply.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/SessionMessage'
                  - $ref: '#/components/schemas/SessionGeneration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
    get:
      tags: [Sessions]
      summary: Read the session's transcript
      description: >
        The session's messages, oldest first. naturali stores no message
        bodies — the dialogue lives in the backing runtime conversation the
        session maps to, so this reads through to the runtime. Pagination is
        `limit`/`offset` rather than an opaque cursor because the upstream is
        offset-based over a stable `position` ordering. This is the one way
        to read back a session opened directly through this API (no
        [Channels](/docs/modules/channels) conversation involved) — see
        `GET .../channels/{channel_id}/conversations/{conversation_id}/messages`
        for the channel-backed equivalent.
      operationId: listSessionMessages
      parameters:
        - $ref: '#/components/parameters/MessagesLimit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: A page of messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionMessageList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/agents/{agent_id}/sessions/{session_id}/generate:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/AgentId'
      - $ref: '#/components/parameters/SessionId'
    post:
      tags: [Sessions]
      summary: Generate a response
      description: >
        Runs the agent over the session's accumulated messages.


        Background by default: this returns `202` immediately and the turn runs
        on. The reply lands in the transcript, so poll
        [`GET /v1/projects/{project_id}/agents/{agent_id}/sessions/{session_id}/messages`](/docs/api/sessions/list-session-messages)
        for the assistant message, or read the session for its `status`.


        Pass `?wait=true` to block instead and receive the turn itself:
        `status` is `completed` with the assistant `message`, or
        `requires_action` with the pending `required_action` tool calls.


        `model` overrides the agent's default model for this turn only.
      operationId: generateSessionResponse
      parameters:
        - $ref: '#/components/parameters/Wait'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SessionGenerate'
      responses:
        '202':
          description: >
            Accepted — the default. The turn is running in the background; its
            reply will appear in the session transcript.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AcceptedSessionGeneration'
        '200':
          description: >
            The settled turn — the agent's reply or a requires_action turn.
            Returned only with `wait=true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionGeneration'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          description: The session has expired due to inactivity.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Client-supplied key to make this mutating POST idempotent.
      schema:
        type: string
    Wait:
      name: wait
      in: query
      required: false
      description: >-
        When omitted or `false` (the default), the turn runs in the background
        and this returns `202`; its reply lands in the session transcript. Pass
        `true` to block until the turn settles and receive it inline.
      schema:
        type: boolean
        default: false
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
    AgentId:
      name: agent_id
      in: path
      required: true
      description: Agent public ID (agent_ prefix).
      schema:
        type: string
        example: agent_V1StGXR8Z5jdHi6B
    SessionId:
      name: session_id
      in: path
      required: true
      description: Session public ID (sess_ prefix).
      schema:
        type: string
        example: sess_V1StGXR8Z5jdHi6B
    MessagesLimit:
      name: limit
      in: query
      required: false
      description: Maximum messages per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Offset:
      name: offset
      in: query
      required: false
      description: Number of messages to skip.
      schema:
        type: integer
        minimum: 0
        default: 0
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: The resource does not exist (existence is not leaked).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: The request conflicts with the current state of the resource.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UpstreamUnavailable:
      description: The upstream runtime could not complete the operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    SessionCreate:
      type: object
      description: Optional configuration for the session being opened.
      properties:
        name:
          type: string
          description: Human-readable label for the session.
          example: support-thread-42
        actor_id:
          type: string
          description: >
            Public ID of an existing actor to attach as the user actor — a
            runtime id showing through the public contract.
          example: actor_V1StGXR8Z5jdHi6B
          x-naturali-ref: actor
        auto_generate:
          type: boolean
          description: >
            When true, adding a user message automatically triggers a
            generation (unless one is already in progress).
          example: false
        tool_context:
          $ref: '#/components/schemas/ToolContext'
        inactivity_ttl_seconds:
          type: integer
          minimum: 0
          description: >
            Seconds of inactivity after which the session expires. 0 means it
            never expires.
          example: 3600
        message_delay_seconds:
          type: integer
          minimum: 0
          nullable: true
          description: >
            Seconds to wait after the last user message before generating — a
            debounce that each new message resets. null means no delay.
          example: null
    Session:
      type: object
      properties:
        id:
          type: string
          description: Public session ID (sess_ prefix).
          example: sess_V1StGXR8Z5jdHi6B
        project_id:
          type: string
          description: The project that owns the session.
          example: proj_V1StGXR8Z5jdHi6B
          x-naturali-ref: project
        agent_id:
          type: string
          description: The agent the session runs against.
          example: agent_V1StGXR8Z5jdHi6B
          x-naturali-ref: agent
        conversation_id:
          type: string
          nullable: true
          description: The underlying runtime conversation backing the session.
        status:
          type: string
          nullable: true
          enum: [open, closed, expired]
          description: Session lifecycle state.
          example: open
        name:
          type: string
          nullable: true
        actor_id:
          type: string
          nullable: true
          x-naturali-ref: actor
        auto_generate:
          type: boolean
          nullable: true
        tool_context:
          type: object
          nullable: true
          additionalProperties:
            type: string
          description: Tool-call context headers, or null when unset.
        inactivity_ttl_seconds:
          type: integer
          nullable: true
        message_delay_seconds:
          type: integer
          nullable: true
        last_activity_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the last message or generation on the session.
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - project_id
        - agent_id
        - status
    SessionMessageCreate:
      type: object
      description: >
        The user message to append — exactly one of `message` or `document_id`.
      properties:
        message:
          type: string
          minLength: 1
          description: User message text.
          example: What is the capital of France?
        document_id:
          type: string
          description: Public ID of a document to use as the message content.
          example: doc_V1StGXR8Z5jdHi6B
          x-naturali-ref: document
        tool_context:
          $ref: '#/components/schemas/ToolContext'
        idempotency_key:
          type: string
          description: >
            Deduplication key scoped to the session; a repeat returns the
            original message with no new work.
          example: inbound-msg-42
    SessionMessage:
      type: object
      description: A message saved to the session.
      properties:
        role:
          type: string
          enum: [user]
          example: user
        content:
          type: string
          nullable: true
          description: The message text, when the message was plain text.
        document_id:
          type: string
          nullable: true
          description: The referenced document, when the message was a document.
          x-naturali-ref: document
    SessionTranscriptMessage:
      type: object
      description: One message in the session's transcript, as the runtime records it.
      properties:
        document_id:
          type: string
          nullable: true
          description: The runtime document holding the message text.
        role:
          type: string
          nullable: true
          enum: [user, assistant, system, null]
          example: user
        content:
          type: string
          nullable: true
          example: What is the capital of France?
        position:
          type: integer
          nullable: true
          description: Zero-based position in the session.
          example: 0
        actor_id:
          type: string
          nullable: true
          description: The actor who authored the message, when set.
        agent_id:
          type: string
          nullable: true
          description: The agent that produced the message, for assistant turns.
        metadata:
          type: object
          nullable: true
          additionalProperties: true
    SessionMessageList:
      type: object
      required: [data, total, limit, offset]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SessionTranscriptMessage'
        total:
          type: integer
          nullable: true
          description: Total messages in the session, when the upstream reports it.
          example: 4
        limit:
          type: integer
          example: 50
        offset:
          type: integer
          example: 0
    SessionGenerate:
      type: object
      description: Optional per-turn overrides for the generation.
      properties:
        model:
          type: string
          description: Overrides the agent's default model for this turn only.
          example: deepseek.v3.2
        tool_context:
          $ref: '#/components/schemas/ToolContext'
    AcceptedSessionGeneration:
      type: object
      description: >
        The handle returned when a session turn is left to run in the
        background — the default. The runtime names the session rather than the
        turn here, so the reply is read from the transcript rather than polled
        by generation id.
      required:
        - status
        - session_id
      properties:
        status:
          type: string
          enum: [accepted]
          description: Always `accepted` — the turn was queued, not settled.
          example: accepted
        session_id:
          type: string
          description: The session the turn is running against.
          example: sess_V1StGXR8Z5jdHi6B
    SessionGeneration:
      type: object
      description: The agent's reply to a session turn.
      properties:
        status:
          type: string
          nullable: true
          enum: [completed, requires_action]
          description: >
            `completed` when the turn finished; `requires_action` when it paused
            on client tool calls (see required_action).
          example: completed
        message:
          type: object
          nullable: true
          description: The assistant message, when completed.
          properties:
            role:
              type: string
              example: assistant
            content:
              type: string
              example: Paris
            model:
              type: string
              example: deepseek.v3.2
        generation_id:
          type: string
          nullable: true
          x-naturali-ref: generation
          description: >
            Public generation ID (gen_ prefix). Read the record with
            `GET /v1/projects/{project_id}/generations/{generation_id}`.
          example: gen_V1StGXR8Z5jdHi6B
        trace_id:
          type: string
          nullable: true
          x-naturali-ref: trace
          description: >
            Trace grouping the generation's tool calls and steps. Read it with
            `GET /v1/projects/{project_id}/traces/{trace_id}` — or its whole
            execution tree with `…/traces/{trace_id}/tree`.
          example: trace_V1StGXR8Z5jdHi6B
        required_action:
          type: object
          nullable: true
          description: Pending client tool calls, when status is requires_action.
          properties:
            tool_calls:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                  tool_name:
                    type: string
                  args:
                    type: object
                    additionalProperties: true
      required:
        - status
        - message
        - generation_id
        - trace_id
        - required_action
    ToolContext:
      type: object
      description: >
        Context forwarded to every `http`/`mcp` tool call made during the session
        or generation: each entry is sent as an `X-Naturali-Context-<key>`
        request header, so a per-user credential can reach a tool endpoint
        without being written into the prompt.


        A key becomes an HTTP header name verbatim — no character is re-cased, so
        `ocaToken` and `oca_token` are two different keys — and must contain only
        letters, digits and `!#$%&'*+-.^_`|~`; two keys that differ only in case
        are rejected, since HTTP folds them into one. To land a value in a header
        the target already expects (usually `Authorization`), have the tool
        declare it with a `{{context:<key>}}` reference in its own `headers`, and
        set the tool's `context_keys` so the value reaches that tool alone.
      additionalProperties:
        type: string
      example:
        tenant: acme
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: upstream_unavailable
            message:
              type: string
              example: Could not create the session on the upstream runtime.
            details:
              type: object
              additionalProperties: true
              description: Optional structured context for the error.
