openapi: 3.0.3
info:
  title: naturali.ai — Projects API
  version: 1.0.0
  description: >
    Projects are the isolation and billing boundary. An account holds multiple
    projects; every resource belongs to exactly one (P2). API keys are
    project-scoped by default. See API.md §4 (Foundations — Projects).
  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: Projects
    description: Create and manage projects — the per-client / per-environment boundary.
security:
  - bearerAuth: []
paths:
  /v1/projects:
    get:
      tags: [Projects]
      summary: List projects
      description: Lists projects accessible to the caller.
      operationId: listProjects
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of projects.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Projects]
      summary: Create a project
      description: Creates a project (one per client or per environment).
      operationId: createProject
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectCreate'
      responses:
        '201':
          description: Project created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/projects/{project_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    get:
      tags: [Projects]
      summary: Get a project
      description: >
        Returns one project. An id you do not own — including one that does not
        exist — responds `404`, not `403`: the API never confirms that an id
        exists elsewhere.

        `403` is reserved for the one case where there is nothing to hide: a
        project you *do* own, addressed with a credential scoped to a different
        one. There the wrong-credential message is what makes the failure
        fixable.
      operationId: getProject
      responses:
        '200':
          description: Project details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Projects]
      summary: Update a project
      description: >
        Rename or archive a project, and/or change its content-retention
        settings (`trace_content_retention_days`, `trace_content_mode`).
        Archiving is reversible; resources are retained.

        The two retention controls answer different questions. The window
        bounds how long content *stays* — a daily sweep purges anything past
        it, leaving auditable skeletons behind. `trace_content_mode: none`
        means content is never *written*, which is the stronger guarantee: it
        cannot be missed by a sweep or survive in a backup.
      operationId: updateProject
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectUpdate'
      responses:
        '200':
          description: Project updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Projects]
      summary: Delete a project
      description: >
        Permanently deletes the project and its backing runtime project. Fails with
        409 if the runtime project still has dependent resources — remove them
        first, or pass `force=true` to delete the project and all its dependents
        (agents, providers, tools, sessions, generations, traces). Forcing is
        destructive and irreversible.
      operationId: deleteProject
      parameters:
        - $ref: '#/components/parameters/Force'
      responses:
        '204':
          description: Project deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: The project still has dependent resources on the runtime (retry with `force=true`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /v1/projects/{project_id}/usage:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    get:
      tags: [Projects]
      summary: Get per-project usage
      description: >
        The per-project meter — the re-billing view (A11/C12/P3). Aggregates the
        project's usage over an optional [from, to] window, bucketed by a single
        dimension. Costs are the billing-grade cost_usd the runtime freezes at write
        time; null means nothing in the bucket was priced (never that it was
        free). Only managed providers are priced (on the runtime), so cost reflects
        managed usage; BYOK usage carries no LLM cost.


        Every bucket also carries components — the amounts actually measured. The
        token counts describe LLM usage alone, so that is where a storage,
        api_request or compute_execution bucket reports its real quantity instead
        of zeroed token fields.
      operationId: getProjectUsage
      parameters:
        - name: group_by
          in: query
          required: false
          description: Dimension to bucket by.
          schema:
            type: string
            enum: [model, agent, run, day, meter_type]
            default: model
        - name: meter_type
          in: query
          required: false
          description: >
            Narrow the rollup to one meter type (llm_tokens, compute_execution,
            api_request, storage). Omit to include every meter. Useful with
            group_by=model, whose dimension otherwise mixes model ids with
            platform SKUs. An unrecognized value yields an empty rollup, not an
            error.
          schema:
            type: string
            example: llm_tokens
        - name: from
          in: query
          required: false
          description: Inclusive lower bound (ISO-8601) on event time. Omit for no lower bound.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: Inclusive upper bound (ISO-8601) on event time. Omit for no upper bound.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Usage for the project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectUsage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum items per page — an integer from 1 to 100 (default 20).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque pagination cursor from a previous response's next_cursor.
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Client-supplied key to make this mutating POST idempotent.
      schema:
        type: string
    Force:
      name: force
      in: query
      required: false
      description: >
        When true, delete the resource together with its dependents instead of
        returning 409. Destructive and irreversible.
      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
  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'
    Forbidden:
      description: >
        Authenticated and the owner, but this credential is scoped to another
        project.
      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'
  schemas:
    Project:
      type: object
      properties:
        id:
          type: string
          description: Public project ID (proj_ prefix).
          example: proj_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: acme-perpetual
        status:
          type: string
          enum: [active, archived]
          example: active
        trace_content_retention_days:
          type: integer
          nullable: true
          minimum: 1
          description: >
            How long trace and generation content is kept before a daily sweep
            purges it. `null` (the default) disables retention, so content is
            kept until purged on demand. A swept record is content-purged the
            same way an on-demand purge does it — the row survives as an
            auditable skeleton with `content_redacted_at` set.
          example: null
        trace_content_mode:
          type: string
          enum: [full, none]
          description: >
            Whether trace and generation content is persisted at all. `full`
            (the default) stores it; `none` is zero-retention — content is
            never written, for every agent in this project. `none` is a floor,
            not a default: an agent may tighten itself to `none`, but cannot
            loosen a `none` project back to `full`.
          example: full
        created_at:
          type: string
          format: date-time
          example: '2026-07-17T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-07-17T00:00:00.000Z'
      required:
        - id
        - name
        - status
        - trace_content_retention_days
        - trace_content_mode
        - created_at
        - updated_at
    ProjectCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Human-readable project name.
          example: acme-perpetual
    ProjectUpdate:
      type: object
      description: At least one field must be present.
      minProperties: 1
      properties:
        name:
          type: string
          example: acme-renamed
        status:
          type: string
          enum: [active, archived]
          example: archived
        trace_content_retention_days:
          type: integer
          nullable: true
          minimum: 1
          description: >
            Days of trace and generation content retention before the daily
            sweep purges it. Send `null` to disable retention (content is then
            kept until purged on demand). Omitting the field leaves the current
            window unchanged — `null` and absent are different instructions.
          example: 90
        trace_content_mode:
          type: string
          enum: [full, none]
          description: >
            Set `none` for zero-retention: content is never written for any
            agent in this project. Tightening to `none` does not erase content
            already on disk — purge it explicitly, or set a retention window to
            have the sweep do it.
          example: none
    UsageTokens:
      type: object
      description: Token counts (input_tokens already includes cached input).
      properties:
        input_tokens:
          type: integer
          example: 120000
        output_tokens:
          type: integer
          example: 45000
        cached_tokens:
          type: integer
          example: 30000
        total_tokens:
          type: integer
          description: input_tokens + output_tokens.
          example: 165000
      required: [input_tokens, output_tokens, cached_tokens, total_tokens]
    UsageComponent:
      type: object
      description: >
        One measured amount inside a bucket. The token counts describe LLM usage
        alone, so this is where a bucket metering storage, requests or compute
        execution reports what it actually measured — without it such a bucket
        would read as all zeros, indistinguishable from one that measured
        nothing.
      properties:
        component:
          type: string
          description: >
            The measured dimension — input_tokens, cached_tokens, output_tokens,
            reasoning_tokens, compute_second, request, gb_day, …
          example: gb_day
        unit:
          type: string
          description: The unit quantity is measured in.
          example: gb_day
        quantity:
          type: number
          description: >
            The summed measured amount, in unit. Fractional for measures like
            GB-days and compute seconds.
          example: 0.4
        cost_usd:
          type: number
          nullable: true
          description: >
            Billing-grade cost in USD for this component; null when it was not
            priced. The quantity is still measured — null never means free.
          example: null
      required: [component, unit, quantity, cost_usd]
    UsageComponents:
      type: object
      description: The measured amounts in a bucket, one entry per component.
      properties:
        components:
          type: array
          description: >
            Sorted by component, then unit. Empty when nothing was measured in
            the bucket.
          items:
            $ref: '#/components/schemas/UsageComponent'
      required: [components]
    UsageGroup:
      allOf:
        - type: object
          properties:
            key:
              type: string
              nullable: true
              description: >
                The bucket's value in the chosen dimension (model id, meter type,
                agent/run id, or YYYY-MM-DD day); null when it does not apply.
              example: anthropic.claude-haiku-4-5-20251001-v1:0
            cost_usd:
              type: number
              nullable: true
              description: Billing-grade cost in USD; null when nothing in the bucket was priced.
              example: 1.23
          required: [key, cost_usd]
        - $ref: '#/components/schemas/UsageTokens'
        - $ref: '#/components/schemas/UsageComponents'
    ProjectUsage:
      allOf:
        - type: object
          properties:
            project_id:
              type: string
              x-naturali-ref: projects
              example: proj_V1StGXR8Z5jdHi6B
            window:
              type: object
              description: The [from, to] bounds applied, echoed back (null = unbounded).
              properties:
                from:
                  type: string
                  format: date-time
                  nullable: true
                  example: null
                to:
                  type: string
                  format: date-time
                  nullable: true
                  example: null
              required: [from, to]
            group_by:
              type: string
              enum: [model, agent, run, day, meter_type]
              example: model
            meter_type:
              type: string
              nullable: true
              description: The meter-type filter applied, echoed back; null when unfiltered.
              example: null
            cost_usd:
              type: number
              nullable: true
              description: Total billing-grade cost in USD; null when nothing was priced.
              example: 1.23
            groups:
              type: array
              description: One entry per distinct value in the chosen dimension.
              items:
                $ref: '#/components/schemas/UsageGroup'
          required: [project_id, window, group_by, meter_type, cost_usd, groups]
        - $ref: '#/components/schemas/UsageTokens'
        - $ref: '#/components/schemas/UsageComponents'
    ProjectList:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Project'
        next_cursor:
          type: string
          nullable: true
          description: Cursor for the next page, or null at the end.
          example: null
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: >
                Machine-readable error code. Fail-closed codes include
                autonomy_class_d, approval_required, guardrail_failed,
                budget_exceeded and access_denied (API.md §1).
              example: access_denied
            message:
              type: string
              example: You do not have access to this project.
            details:
              type: object
              additionalProperties: true
              description: Optional structured context for the error.
