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).
  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: []
  - oauth2:
      - mcp:access
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.

        A project-scoped key requires membership of that project. The key
        carries no role of its own — every request it makes resolves its
        holder's membership again — so it can never reach past what its minter
        already had. An account-scoped key requires a credential that is not
        itself confined to one project.

        A connected app cannot create keys at all: a grant is revocable and a
        key is not, so a key minted under a grant would still work after the app
        was disconnected. Reading and revoking keys stay available.
      operationId: createApiKey
      security:
        - bearerAuth: []
      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'
        '403':
          $ref: '#/components/responses/ApiKeyForbidden'
  /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/ApiKeyForbidden'
        '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/ApiKeyForbidden'
        '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/ApiKeyForbidden'
        '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.

        Refused for a connected app, like creation: rotation returns a raw
        secret for a key the caller need never have held, so it reaches the same
        place by another door.
      operationId: rotateApiKey
      security:
        - bearerAuth: []
      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/ApiKeyForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
    oauth2:
      type: oauth2
      description: >-
        A connected app's OAuth access token, issued by this API's authorization
        server (discovery: /.well-known/oauth-authorization-server). Its one scope
        carries every operation, confined to the projects the user chose when
        approving the app.
      flows:
        authorizationCode:
          authorizationUrl: https://api.naturali.ai/authorize
          tokenUrl: https://api.naturali.ai/token
          refreshUrl: https://api.naturali.ai/token
          scopes:
            mcp:access: Every operation this API serves, on the projects the grant covers.
  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
    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'
    ApiKeyForbidden:
      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
          description: >-
            Structured error. Every error response uses this shape, so `code` can
            be read without first checking the type of `error`.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: >-
                Machine-readable error code: lower_snake for an error naturali
                raises (`access_denied`), UPPER_SNAKE for one the runtime reports
                (`RESOURCE_NOT_FOUND`).
              example: access_denied
            message:
              type: string
              description: Human-readable explanation.
              example: Your role in this project does not carry this action.
            details:
              type: object
              additionalProperties: true
              description: >-
                Structured context for an error naturali raises, such as the
                `resource` and `limit` of a `plan_limit_reached`.
            meta:
              type: object
              description: Structured context for an error the runtime reports.
