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

# Complete one conversational turn

> Resolves the authenticated caller's agent and thread, retrieves memory context, generates the answer, records the turn, and returns source citations as one API-owned operation.




## OpenAPI

````yaml /openapi.yaml post /v1/agents/{agent_id}/threads/{thread_id}/chat
openapi: 3.0.3
info:
  title: Priors API
  description: >
    Priors gives you memory agents. An agent learns from the observations you
    send it and answers from what it learned.


    You call this API from your own backend with a partner API key. The key
    identifies your organization, which owns every agent, thread, observation,
    source, and memory it creates. Another organization receives 404 for those
    identifiers. Send `X-SP-End-User` alongside the key to act for one named
    person on your side.
  version: 0.1.0
servers:
  - url: https://app.sapientpriors.com/api
    description: Production
security:
  - partnerKey: []
tags:
  - name: Agents
    description: Create, read, update, and delete memory agents.
  - name: Threads
    description: 'Conversations under an agent: create them, teach them, and ask them.'
  - name: Sources
    description: Files an agent learned from, and the citations that point back to them.
  - name: Authentication
    description: What your key resolves to, and the limits that apply to it.
  - name: Billing
    description: Your organization's credits and charges.
paths:
  /v1/agents/{agent_id}/threads/{thread_id}/chat:
    post:
      tags:
        - Threads
      summary: Complete one conversational turn
      description: >
        Resolves the authenticated caller's agent and thread, retrieves memory
        context, generates the answer, records the turn, and returns source
        citations as one API-owned operation.
      operationId: chat
      parameters:
        - $ref: '#/components/parameters/AgentId'
        - $ref: '#/components/parameters/ThreadId'
        - $ref: '#/components/parameters/EndUser'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatRequest'
      responses:
        '200':
          description: Completed conversational turn
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/AgentNotReady'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
components:
  parameters:
    AgentId:
      name: agent_id
      in: path
      required: true
      description: Unique managed memory-agent identifier.
      schema:
        type: string
        maxLength: 32
        pattern: ^agent_[0-9A-HJKMNP-TV-Z]{26}$
    ThreadId:
      name: thread_id
      in: path
      required: true
      description: Unique thread identifier scoped to the agent.
      schema:
        type: string
        maxLength: 33
        pattern: ^thread_[0-9A-HJKMNP-TV-Z]{26}$
    EndUser:
      name: X-SP-End-User
      in: header
      required: false
      description: >
        Your own identifier for the person this request acts for: an account
        number, a row id, anything stable on your side. It works only with a
        partner API key. The value is not stored. Priors hashes it together with
        your organization id, so two clients that send the same string are still
        two different people.


        With the header, you reach the threads that same person created and no
        others. A new thread is recorded against that person, and `GET
        /v1/agents/{agent_id}` returns that person's conversations only. Without
        the header, the key acts as the organization and reaches every thread on
        every agent it owns. Learned memory is shared across the agent either
        way. Only the thread list narrows.


        Sent with anything that is not a partner API key, the header returns 400
        instead of being ignored. Sent empty or as whitespace, it also returns
        400, and it is never read as absent: absent means the whole
        organization, which is wider access than you asked for. Surrounding
        whitespace is removed. The 255-character limit is measured on the value
        as sent.
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: \S
  schemas:
    ChatRequest:
      type: object
      required:
        - turn_id
        - message
      properties:
        turn_id:
          type: string
          minLength: 1
          maxLength: 200
        message:
          type: string
          minLength: 1
          maxLength: 2000
        timestamp:
          type: string
          format: date-time
          nullable: true
        image_source_id:
          allOf:
            - $ref: '#/components/schemas/ImageSourceId'
          nullable: true
        image_source_ids:
          type: array
          maxItems: 10
          nullable: true
          items:
            $ref: '#/components/schemas/ImageSourceId'
        detail:
          $ref: '#/components/schemas/Detail'
      additionalProperties: false
    ChatResponse:
      type: object
      required:
        - agent_id
        - thread_id
        - turn_id
        - message
        - memory_context
        - model
        - duration_ms
        - stages
        - citations
      properties:
        agent_id:
          $ref: '#/components/schemas/AgentId'
        thread_id:
          $ref: '#/components/schemas/ThreadId'
        turn_id:
          type: string
        message:
          type: string
        memory_context:
          type: string
        model:
          type: string
        duration_ms:
          type: integer
          minimum: 0
        stages:
          type: array
          items:
            $ref: '#/components/schemas/ChatStage'
        citations:
          type: array
          items:
            $ref: '#/components/schemas/Citation'
        learning:
          type: array
          items:
            $ref: '#/components/schemas/LearningItem'
          description: >
            Turns in this thread that were not learned: writing their memory
            failed after an earlier response was already returned, and no
            earlier response reported them. Each one is reported at least once.
            `GET .../threads/{thread_id}/learning` is the full record. Empty
            when every turn so far was learned or is still being learned.
      additionalProperties: false
    ImageSourceId:
      type: string
      pattern: ^img_[0-9a-f]{32}$
      description: Server-generated image-source identifier accepted by chat.
    Detail:
      type: string
      enum:
        - brief
        - standard
        - detailed
      default: standard
      description: >
        How much the agent writes: the length of `response_context` on
        `observe-and-enrich` and `observe-and-respond`, and of the answer on
        `chat`. `brief`: only the most important facts, in 2 to 4 sentences; use
        it for short replies such as a chat widget, a notification, or an SMS.
        `standard` (the default): the normal length; use it for most questions.
        `detailed`: every relevant fact, date, name, number, and direct quote
        the memories hold, with a list or table when the question asks for one
        or there are many items; use it for reports, audits, the history of a
        customer, and "list everything" questions. This is an instruction to the
        agent, not a cut: the text is never trimmed to fit. For a hard limit on
        size, use `max_context_chars`.
    AgentId:
      type: string
      pattern: ^agent_[0-9A-HJKMNP-TV-Z]{26}$
      description: Server-generated agent identifier.
    ThreadId:
      type: string
      pattern: ^thread_[0-9A-HJKMNP-TV-Z]{26}$
      description: Server-generated thread identifier.
    ChatStage:
      type: object
      required:
        - name
        - duration_ms
      properties:
        name:
          type: string
          enum:
            - memory_context
            - final_generation
            - turn_learning
        duration_ms:
          type: integer
          minimum: 0
      additionalProperties: false
    Citation:
      type: object
      required:
        - source_id
        - label
        - media_type
        - href
        - verified
      properties:
        source_id:
          $ref: '#/components/schemas/SourceId'
        label:
          type: string
        media_type:
          type: string
        href:
          type: string
          description: Same-origin authenticated download path for the source.
        verified:
          type: boolean
          description: >
            True when the model looked at this source on this turn, which is the
            case for an image attached to the turn. False when the link comes
            from a memory the source produced earlier.
      additionalProperties: false
    LearningItem:
      type: object
      description: >
        Whether one turn of a request was learned into memory. A turn is not
        learned when writing its memory failed after the response was returned.
        It is never learned later: send it again to have it learned.
      required:
        - request_id
        - role
        - status
      properties:
        request_id:
          type: string
          description: >
            The id you know the request by: the `request_id` you sent to
            `observe-and-respond` or `observe-and-enrich` (or the `x-request-id`
            header of its response), or the `turn_id` of a `chat` message.
        role:
          type: string
          enum:
            - user
            - assistant
          description: >
            `user` is the turn you sent. `assistant` is the answer Priors wrote
            and learned (`chat`, and `observe-and-respond` with `learn_reply`).
        status:
          type: string
          enum:
            - learned
            - pending
            - not_learned
          description: >
            `learned`: in memory. `pending`: still being learned. `not_learned`:
            will never be learned; send it again. The `learning` lists on
            responses carry only `not_learned`.
        reason:
          type: string
          nullable: true
          description: >
            Stable machine code for why a turn was not learned, for example
            `generation_failed`, `model_unavailable`, `generation_timeout`,
            `generation_unparseable`, `memory_commit_shortfall`,
            `embedder_mismatch`, or `earlier_turn_failed` (dropped because a
            turn before it failed). Treat an unknown code like
            `generation_failed`. Null unless `not_learned`.
        message:
          type: string
          nullable: true
          description: A sentence to show a person. Null unless `not_learned`.
        refunded_credits:
          type: string
          nullable: true
          pattern: ^-?\d+(\.\d+)?$
          description: >
            Credits refunded for this turn, as a decimal string. Null when
            nothing was refunded for it: nothing was charged, or it is an answer
            Priors wrote, which was delivered and stays charged.
        action:
          type: string
          nullable: true
          enum:
            - resend
            - null
          description: '`resend` when `not_learned`, else null.'
      additionalProperties: false
    ErrorResponse:
      type: object
      required:
        - error
        - code
        - request_id
      properties:
        error:
          type: string
        code:
          type: string
          description: >
            Machine-readable condition. Branch on this rather than on the status
            where the two differ. All three of these can arrive as a 5xx and
            they want different handling: `service_unavailable` (503) is a
            dependency outage worth retrying, `service_not_configured` (503) is
            the same transport failure that retrying will NEVER clear because a
            feature is not switched on for your organization, and
            `internal_error` (500) is a defect that will fail the same way
            again. Known values: invalid_request, unauthorized, forbidden,
            payment_required, not_found, agent_not_found, thread_not_found,
            conflict, agent_quota_exceeded, duplicate_upload_in_progress,
            document_already_ingested, document_uploaded_elsewhere,
            agent_not_ready, payload_too_large, rate_limit_exceeded,
            service_unavailable, service_not_configured,
            answer_model_unavailable, internal_error, and the document codes an
            upload can return (unsupported_document, document_needs_ocr,
            encrypted_document, malformed_document, empty_document,
            document_too_large, document_too_complex, document_too_many_turns,
            document_too_many_pages, document_too_large_for_ocr,
            document_pages_unexpected, document_service_unavailable, ocr_busy,
            ocr_failed, ingestion_failed).


            answer_model_unavailable is a narrower `service_unavailable` (503):
            the model that writes the visible reply could not be reached after
            retries. It is worth its own code because it is the one dependency
            whose outage a caller may want to word differently -- the turn
            produced no answer at all, and retrying shortly is the right advice.


            agent_not_found and thread_not_found are narrower `not_found`s, sent
            so a client can tell WHICH resource vanished and recover accordingly
            without matching on the English message. Treat any unrecognised
            value as the status code's default meaning rather than rejecting the
            response.
        request_id:
          type: string
          description: >
            Server-issued identifier for this request. Include it when reporting
            a problem so the call can be correlated with server logs.
        details:
          type: string
          description: |
            Present on a 429. Names which control refused the request:
              `user`                    Your per-subject window, counted against `X-SP-End-User`.
              `organization`            Your organization's ceiling, across every key you hold.
              `capacity`                The service is at capacity. Retry.
              `ingestion-organization`  Too much of your own document work is already running.
              `ingestion-capacity`      As above, service-wide.
      additionalProperties: false
    SourceId:
      type: string
      pattern: ^(?:img|src)_[0-9a-f]{32}$
      description: Server-generated uploaded-source identifier.
  responses:
    BadRequest:
      description: >
        The request is not one this route can act on, and sending it again
        unchanged does not help. Usually the payload: a field missing, or a
        value out of range.


        It is also the credential and the headers around it, and each of these
        is refused rather than ignored: `X-SP-End-User` sent empty, or longer
        than 255 characters; and a key on a request that carries
        `Sec-Fetch-Site`, which only a browser sets.


        `code` is `invalid_request` throughout. `error` says which of these it
        was.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: >
        No such id, or it is not one this key reaches. It is never a 403, so a
        key cannot be used to learn which ids exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    AgentNotReady:
      description: Agent exists but its dependencies are still initializing or failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    TooManyRequests:
      description: >
        A rate window or an admission control refused the request. `Retry-After`
        carries the seconds to wait, and `details` names which control refused.
        Nothing was learned and nothing was charged.
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Always at least 1.
          required: true
          schema:
            type: integer
            minimum: 1
        X-RateLimit-Limit:
          description: Requests the refusing window admits in its period.
          schema:
            type: integer
            minimum: 1
        X-RateLimit-Remaining:
          description: Requests left in the current window. Zero on a refusal.
          schema:
            type: integer
            minimum: 0
        X-RateLimit-Reset:
          description: Unix timestamp (seconds) at which the window admits again.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServiceUnavailable:
      description: >
        A required dependency could not complete the request. Distinct from a
        500 by design: the body's `code` is `service_unavailable`, which says
        the request may succeed on a later attempt, where `internal_error` says
        it will not.


        Check the code before retrying. The same 503 carries
        `service_not_configured` when a feature this route needs is not switched
        on for your organization. The transport failure is identical and the
        wait is not: that one answers the same on every attempt until we change
        the configuration. Contact us.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    partnerKey:
      type: http
      scheme: bearer
      description: >
        Your API key, shaped `sp_<key_id>_<secret>`, sent as `Authorization:
        Bearer sp_...`. It identifies your organization, not a person. Your
        organization owns the agents, holds the credit, and is billed. Every key
        your organization holds reaches the same things, so you rotate a key by
        minting the new one, deploying it, and revoking the old one.


        Keep the key on your server. A request that carries a `Sec-Fetch-Site`
        header is refused with 400, because only a browser sets that header.
        Priors sends no CORS headers, so a browser blocks the call before it
        leaves the page. The secret is shown once when the key is minted and is
        stored only as a hash. A lost secret is replaced, never recovered.


        An administrator of your organization mints and revokes keys in the
        Priors application, under Settings. There is no endpoint for it: a key
        reaches your whole organization, so issuing one is an act of
        administration.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.