openapi: 3.0.3
info:
  title: naturali.ai — Actors API
  version: 1.0.0
  description: >
    An **actor** is who an agent is talking to: the end user on the other side
    of a session. It carries the identity a generation is attributed to, the
    persona instructions composed into that turn's prompt, and — when asked
    for — a memory container that follows the actor across sessions.

    An actor is keyed by your own `external_id`: whatever your application
    already calls that person (a user id, an account number, a hashed email).
    Creating one is idempotent on that key, so the first thing your backend
    does when a new user appears can be this call, with no read first and no
    duplicate on a retry.

    Actors are for traffic that arrives through this API. A conversation that
    arrives through a channel resolves to an
    [address](/docs/api/addresses/get-address) instead, which mints and owns
    its own actor — so an address's actor is managed through that address, not
    here.
  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: Actors
    description: Identify the end user an agent is talking to.
security:
  - bearerAuth: []
paths:
  /v1/projects/{project_id}/actors:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    get:
      tags: [Actors]
      summary: List actors
      description: >
        Lists the project's actors. Filter by `external_id` to resolve your own
        key to an actor without creating one.
      operationId: listActors
      parameters:
        - $ref: '#/components/parameters/ExternalIdFilter'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: A page of actors.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActorList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
    post:
      tags: [Actors]
      summary: Create an actor
      description: >
        Creates the actor, or returns the one that already carries this
        `external_id`. Idempotent on that key: a retry returns the existing
        actor with `200` rather than creating a second one, so this is safe as
        the first call your backend makes when it sees a new user.

        `external_id` may not start with a channel prefix (`whatsapp:`,
        `discord:`, …) or `address:` — those name actors that belong to an
        [address](/docs/api/addresses/get-address), which owns its own.
      operationId: createActor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ActorCreate'
      responses:
        '200':
          description: An actor with this `external_id` already existed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Actor'
        '201':
          description: Actor created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Actor'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
  /v1/projects/{project_id}/actors/{actor_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/ActorId'
    get:
      tags: [Actors]
      summary: Get an actor
      description: >
        Returns one actor. An actor belonging to another project responds
        `404`, not `403` — the API never confirms that an id exists elsewhere.
      operationId: getActor
      responses:
        '200':
          description: The actor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Actor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
    patch:
      tags: [Actors]
      summary: Update an actor
      description: >
        Updates the fields present in the body and leaves the rest alone.
        `external_id` is not updatable: it is the key callers converge on, and
        moving it would silently orphan every reference they hold.
      operationId: updateActor
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ActorUpdate'
      responses:
        '200':
          description: The updated actor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Actor'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
    delete:
      tags: [Actors]
      summary: Erase an actor
      description: >
        Removes the actor and the sessions it holds. Erasure of one identity as
        this API knows it — narrower than "erase this human everywhere", since
        naturali does not know that two identities are the same person and does
        not claim to.

        An actor that belongs to an
        [address](/docs/api/addresses/get-address) responds `409`: erase it
        through
        `DELETE /v1/projects/{project_id}/addresses/{identifier}`, which also
        removes the address and its conversations. Deleting it here would leave
        those behind, pointing at an identity that no longer exists.
      operationId: deleteActor
      responses:
        '204':
          description: Actor erased.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The actor belongs to an address and is erased through it.
          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:
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
    ActorId:
      name: actor_id
      in: path
      required: true
      description: Actor public ID (actor_ prefix).
      schema:
        type: string
        example: actor_V1StGXR8Z5jdHi6B
    ExternalIdFilter:
      name: external_id
      in: query
      required: false
      description: Return only the actor carrying this external ID.
      schema:
        type: string
        example: user_42
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum items to return (1–100).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Offset:
      name: offset
      in: query
      required: false
      description: >
        Items to skip. This list pages by offset rather than by naturali's usual
        opaque cursor because the upstream ordering is offset-based; a cursor
        here would only imitate a keyset.
      schema:
        type: integer
        minimum: 0
        default: 0
  responses:
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid credentials.
      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'
    UpstreamUnavailable:
      description: The upstream runtime could not complete the operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: actor_owned_by_address
            message:
              type: string
              example: 'This actor belongs to an address; erase it through the address.'
            details:
              type: object
              additionalProperties: true
    Actor:
      type: object
      description: The end user an agent is talking to.
      properties:
        id:
          type: string
          nullable: true
          description: Public actor ID (actor_ prefix).
          example: actor_V1StGXR8Z5jdHi6B
        project_id:
          type: string
          nullable: true
          x-naturali-ref: project
          example: proj_V1StGXR8Z5jdHi6B
        external_id:
          type: string
          nullable: true
          description: >
            Your own key for this person. Null only for an actor minted before
            this resource existed.
          example: user_42
        name:
          type: string
          nullable: true
          description: A human-friendly name, for reading a transcript later.
          example: Ana
        instructions:
          type: string
          nullable: true
          description: >
            Persona instructions composed into the agent's prompt for this
            actor's turns.
          example: 'Speaks Portuguese. Prefers short answers.'
        has_memory:
          type: boolean
          description: >
            Whether a memory container is linked, carrying what the agent
            remembers about this actor across sessions. The container's own id
            is not exposed — naturali fronts no memory resource that could
            resolve it.
          example: false
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - project_id
        - external_id
        - name
        - instructions
        - has_memory
        - created_at
        - updated_at
    ActorCreate:
      type: object
      required: [external_id]
      properties:
        external_id:
          type: string
          minLength: 1
          description: >
            Your own key for this person — the idempotency key this call
            converges on. May not start with a channel prefix (`whatsapp:`,
            `discord:`, …) or `address:`.
          example: user_42
        name:
          type: string
          description: A human-friendly name. Defaults to `external_id`.
          example: Ana
        instructions:
          type: string
          description: >
            Persona instructions composed into the agent's prompt for this
            actor's turns. Treat as trusted input: text an end user controls
            reaches the agent's instructions verbatim.
          example: 'Speaks Portuguese. Prefers short answers.'
        memory:
          type: boolean
          description: >
            Give the actor its own memory container, so an agent remembers it
            across sessions. Applied on creation only — a call that returns an
            existing actor leaves its memory as it was.
          default: false
          example: true
    ActorUpdate:
      type: object
      description: Only the fields present are changed.
      properties:
        name:
          type: string
          example: Ana Souza
        instructions:
          type: string
          nullable: true
          description: Null clears the persona instructions.
          example: 'Speaks Portuguese. Prefers short answers.'
    ActorList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Actor'
        total:
          type: integer
          nullable: true
          description: Total actors matching the query, when the upstream reports it.
          example: 128
        limit:
          type: integer
          example: 20
        offset:
          type: integer
          example: 0
      required: [data, total, limit, offset]
