openapi: 3.0.3
info:
  title: naturali.ai — API Keys API
  version: 1.0.0
  description: >
    Long-lived programmatic credentials (nat_sk_…), created by a logged-in user
    and embedded in a machine (a customer backend, CI, an integration). Scoped
    to a project (default) or the whole account, plus a capability set; hashed
    at rest; the raw secret is returned exactly once, at creation or rotation.
    Interactive human clients authenticate with session JWTs instead (see the
    Auth API). See API.md §1 (Authentication).
  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: API Keys
    description: Create, scope, rotate and revoke nat_sk_ API keys.
security:
  - bearerAuth: []
paths:
  /v1/api-keys:
    get:
      tags: [API Keys]
      summary: List API keys
      description: >
        Lists API keys accessible to the caller. A project-scoped credential
        sees only keys in its project; an account-scoped credential sees all
        keys in the account. Raw secrets are never returned.
      operationId: listApiKeys
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of API keys.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [API Keys]
      summary: Create an API key
      description: >
        Creates an API key. When `project_id` is set the key is scoped to that
        project (the default and recommended stance); omit it for an
        account-scoped key. `capabilities` narrows what the key may do; when
        omitted the key inherits the creator's capabilities. The raw
        `key` (nat_sk_…) is returned only in this response.
      operationId: createApiKey
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreate'
      responses:
        '201':
          description: API key created. The raw key value is only returned here.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/api-keys/{api_key_id}:
    parameters:
      - $ref: '#/components/parameters/ApiKeyId'
    get:
      tags: [API Keys]
      summary: Get an API key
      description: Returns metadata for an API key. The raw secret is never returned after creation.
      operationId: getApiKey
      responses:
        '200':
          description: API key metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRecord'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [API Keys]
      summary: Update an API key
      description: Rename an API key or replace its capability set. The scope (project vs account) is immutable.
      operationId: updateApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyUpdate'
      responses:
        '200':
          description: API key updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyRecord'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [API Keys]
      summary: Revoke an API key
      description: Revokes an API key immediately. Subsequent use returns 401.
      operationId: deleteApiKey
      responses:
        '204':
          description: API key revoked.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/api-keys/{api_key_id}:rotate:
    parameters:
      - $ref: '#/components/parameters/ApiKeyId'
    post:
      tags: [API Keys]
      summary: Rotate an API key
      description: >
        Issues a new secret for the same key record (same id, scope and
        capabilities) and invalidates the previous secret. The new raw `key`
        is returned only in this response.
      operationId: rotateApiKey
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Rotated. The new raw key value is only returned here.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyCreated'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A session access JWT (managing keys) or a naturali API key (nat_sk_…).
  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
    ApiKeyId:
      name: api_key_id
      in: path
      required: true
      description: API key public ID (key_ prefix).
      schema:
        type: string
        example: key_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, but not permitted to act on this key.
      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:
    ApiKeyRecord:
      type: object
      properties:
        id:
          type: string
          description: Public API key ID (key_ prefix).
          example: key_V1StGXR8Z5jdHi6B
        name:
          type: string
          example: CI/CD Pipeline
        key_prefix:
          type: string
          description: Leading characters of the raw key, for identification.
          example: nat_sk_a1b2c3
        scope:
          type: string
          enum: [project, account]
          description: Whether the key is scoped to one project or the whole account.
          example: project
        project_id:
          type: string
          nullable: true
          x-naturali-ref: projects
          description: The project this key is scoped to; null for account-scoped keys.
          example: proj_V1StGXR8Z5jdHi6B
        capabilities:
          type: array
          description: Capabilities granted to this key. Empty means it inherits the creator's.
          items:
            type: string
          example: ['agents:read', 'sessions:write']
        created_at:
          type: string
          format: date-time
          example: '2026-07-17T00:00:00.000Z'
        last_used_at:
          type: string
          format: date-time
          nullable: true
          description: When the key was last presented; null if never used.
          example: '2026-07-17T09:12:00.000Z'
      required: [id, name, key_prefix, scope, capabilities, created_at]
    ApiKeyCreated:
      allOf:
        - $ref: '#/components/schemas/ApiKeyRecord'
        - type: object
          required: [key]
          properties:
            key:
              type: string
              description: The raw API key value (only returned once, at creation or rotation). Use as the Bearer token.
              example: nat_sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
    ApiKeyCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Key name for identification.
          example: CI/CD Pipeline
        project_id:
          type: string
          x-naturali-ref: projects
          description: Project to scope the key to. Omit for an account-scoped key.
          example: proj_V1StGXR8Z5jdHi6B
        capabilities:
          type: array
          items:
            type: string
          description: Capabilities to grant. Omit to inherit the creator's capabilities.
          example: ['agents:read', 'sessions:write']
    ApiKeyUpdate:
      type: object
      description: At least one field must be present. Scope is immutable.
      minProperties: 1
      properties:
        name:
          type: string
          example: Renamed Key
        capabilities:
          type: array
          items:
            type: string
          description: Replace the capability set (empty array clears all).
          example: ['agents:read']
    ApiKeyList:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ApiKeyRecord'
        next_cursor:
          type: string
          nullable: true
          example: null
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: access_denied
            message:
              type: string
              example: You do not own this API key.
            details:
              type: object
              additionalProperties: true
