openapi: 3.0.3
info:
  title: naturali.ai — Listings API
  version: 1.0.0
  description: >
    Listings: a tool or an agent a project publishes for other projects to
    install. The publisher creates a listing, submits it for review, and
    naturali lists it in the marketplace. Other projects install it with the
    Installs API and use it like one of their own; every call runs on the
    publisher's configuration and is recorded and billed in the installing
    project.

    The marketplace (`GET /v1/listings`) is readable by any signed-in caller and
    shows only listed entries. A listing that is not listed yet can still be
    installed by its id, so a publisher can try it from a second project.
  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: Listings
    description: Publish a tool or an agent, and see who installed it.
security:
  - bearerAuth: []
  - oauth2:
      - mcp:access
paths:
  /v1/listings:
    get:
      tags: [Listings]
      summary: Browse the marketplace
      description: >
        Every listed entry, newest first. `interface` is what an installing
        project sees of the resource, read live from the publisher.
      operationId: listPublicListings
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of public listings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicListingList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/listings/{listing_id}:
    parameters:
      - $ref: '#/components/parameters/ListingId'
    get:
      tags: [Listings]
      summary: Get a public listing
      description: One listed entry. A listing that is not listed answers `404`.
      operationId: getPublicListing
      responses:
        '200':
          description: The public listing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicListing'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{project_id}/listings:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
    get:
      tags: [Listings]
      summary: List a project's listings
      description: The listings the project publishes, newest first.
      operationId: listListings
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of listings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListingList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      tags: [Listings]
      summary: Create a listing
      description: >
        Publish one of the project's tools or agents as a `draft`. Any project
        member may publish.

        An agent on a naturali model whose model has no price answers `503
        model_not_priced`. A resource that does not exist in the project
        answers `400 resource_not_found`, and one that already has a listing
        `409 listing_exists`.
      operationId: createListing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListingCreate'
      responses:
        '201':
          description: Listing created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Listing'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ListingConflict'
        '503':
          $ref: '#/components/responses/ListingModelNotPriced'
  /v1/projects/{project_id}/listings/{listing_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/ListingId'
    get:
      tags: [Listings]
      summary: Get a listing
      description: One of the project's listings.
      operationId: getListing
      responses:
        '200':
          description: The listing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Listing'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Listings]
      summary: Update a listing
      description: >
        Change the title or description, or pause and resume the listing.

        `state: suspended` stops the listing for every installing project at
        once; their installs are kept. `state: listed` resumes a listing you
        suspended, provided naturali had listed it; nobody reinstalls. A
        listing naturali suspended cannot be resumed here, and an invalid move
        answers `409 invalid_listing_state`.
      operationId: updateListing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ListingUpdate'
      responses:
        '200':
          description: Listing updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Listing'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ListingConflict'
        '503':
          $ref: '#/components/responses/ListingModelNotPriced'
  /v1/projects/{project_id}/listings/{listing_id}:submit:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/ListingId'
    post:
      tags: [Listings]
      summary: Submit a listing for review
      description: >
        Move a `draft` listing to `in_review`. naturali lists it in the marketplace
        or sends it back to `draft`. Any other state answers `409
        invalid_listing_state`.
      operationId: submitListing
      responses:
        '200':
          description: Listing submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Listing'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/ListingConflict'
        '503':
          $ref: '#/components/responses/ListingModelNotPriced'
  /v1/projects/{project_id}/listings/{listing_id}/installs:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/ListingId'
    get:
      tags: [Listings]
      summary: List a listing's installs
      description: >
        Each project that installed the listing, with its usage this billing
        cycle as last sampled. Never the content of a call.
      operationId: listListingInstalls
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of installs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListingInstallList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{project_id}/listings/{listing_id}/installs/{install_id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/ListingId'
      - $ref: '#/components/parameters/InstallId'
    delete:
      tags: [Listings]
      summary: Remove a project's install
      description: >
        Cut one installing project off. An `active` or `suspended` install
        becomes `blocked`, and that project cannot install the listing again.
        Deleting a `blocked` install removes it, which lets that project
        install again.
      operationId: deleteListingInstall
      responses:
        '204':
          description: Install blocked or removed.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '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
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
    ListingId:
      name: listing_id
      in: path
      required: true
      description: Listing public ID (lst_ prefix).
      schema:
        type: string
        example: lst_V1StGXR8Z5jdHi6B
    InstallId:
      name: install_id
      in: path
      required: true
      description: Install public ID (ins_ prefix).
      schema:
        type: string
        example: ins_V1StGXR8Z5jdHi6B
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: >-
        The credential is scoped to a different project, or the caller's role in
        the project does not carry this action.
      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'
    BadRequest:
      description: The request was malformed or failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ListingConflict:
      description: >
        `listing_exists` (the resource already has a listing) or
        `invalid_listing_state` (the move is not allowed from the listing's
        current state).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ListingModelNotPriced:
      description: >
        `model_not_priced`: the agent runs on a naturali model that has no price
        yet, so its use could not be billed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    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.
    ListingResourceType:
      type: string
      enum: [tool, agent]
      description: What the listing publishes.
      example: tool
    ListingState:
      type: string
      enum: [draft, in_review, listed, suspended]
      description: >
        `draft` until submitted, `in_review` until naturali decides, `listed` in
        the marketplace, `suspended` when paused by its publisher or by naturali.
      example: listed
    Listing:
      type: object
      description: A listing, as its publisher reads it.
      properties:
        id:
          type: string
          example: lst_V1StGXR8Z5jdHi6B
        project_id:
          type: string
          description: The publishing project.
          example: proj_V1StGXR8Z5jdHi6B
        resource_type:
          $ref: '#/components/schemas/ListingResourceType'
        resource_id:
          type: string
          description: The published tool or agent.
          example: tool_V1StGXR8Z5jdHi6B
        title:
          type: string
          example: Invoice OCR
        description:
          type: string
          nullable: true
          example: Reads a scanned invoice and returns its fields.
        pricing:
          type: array
          description: What an installing project is charged per call. Empty is free.
          items:
            $ref: '#/components/schemas/ListingPricingComponent'
        pricing_next:
          $ref: '#/components/schemas/ListingPricingNext'
        state:
          $ref: '#/components/schemas/ListingState'
        created_at:
          type: string
          format: date-time
          example: '2026-10-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-10-01T00:00:00.000Z'
      required:
        - id
        - project_id
        - resource_type
        - resource_id
        - title
        - description
        - pricing
        - pricing_next
        - state
        - created_at
        - updated_at
    ListingPricingComponent:
      type: object
      additionalProperties: false
      required: [component, unit, quantity, unit_price]
      properties:
        component:
          type: string
          pattern: '^[a-z][a-z0-9_]{0,39}$'
          description: >
            The publisher's name for what is charged. Must not be a name the
            runtime already meters, such as `input_tokens` or `tool_call`.
          example: page
        unit:
          type: string
          minLength: 1
          maxLength: 40
          example: count
        quantity:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            JSON Logic giving the quantity `unit_price` multiplies, over
            `{ input, action, response, outcome, duration_ms }` for a tool and
            `{ response: { usage, cost_usd, steps, tool_calls, stop_reason },
            outcome }` for an agent. `null` charges one per call. A failed call
            is never charged a component.
          example: { var: response.page_count }
        unit_price:
          type: number
          minimum: 0
          description: US dollars per `unit`.
          example: 0.002
    ListingPricingNext:
      type: object
      nullable: true
      required: [effective_from, pricing]
      description: >
        A price increase waiting out its notice. Installing projects are
        charged `pricing` until `effective_from`, then this.
      properties:
        effective_from:
          type: string
          format: date-time
          example: '2026-10-08T00:01:00.000Z'
        pricing:
          type: array
          items:
            $ref: '#/components/schemas/ListingPricingComponent'
    ListingList:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Listing'
        next_cursor:
          type: string
          nullable: true
          description: Cursor for the next page, or null when there are no more.
          example: null
    ListingCreate:
      type: object
      additionalProperties: false
      required: [resource_type, resource_id, title]
      properties:
        resource_type:
          $ref: '#/components/schemas/ListingResourceType'
        resource_id:
          type: string
          minLength: 1
          description: A tool or agent in this project.
          example: tool_V1StGXR8Z5jdHi6B
        title:
          type: string
          minLength: 1
          maxLength: 120
          example: Invoice OCR
        description:
          type: string
          nullable: true
          maxLength: 2000
          example: Reads a scanned invoice and returns its fields.
        pricing:
          type: array
          maxItems: 10
          description: >
            What an installing project is charged per call, on top of any
            model cost. Omit for a free listing.
          items:
            $ref: '#/components/schemas/ListingPricingComponent'
    ListingUpdate:
      type: object
      additionalProperties: false
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 120
          example: Invoice OCR
        description:
          type: string
          nullable: true
          maxLength: 2000
          example: Reads a scanned invoice and returns its fields.
        state:
          type: string
          enum: [suspended, listed]
          description: Pause the listing, or resume one you paused.
          example: suspended
        pricing:
          type: array
          maxItems: 10
          description: >
            The new price, replacing the whole list. A lower price applies the
            next minute. A higher unit price or a new component applies 7 days
            later, is returned in `pricing_next` meanwhile, and is emailed to
            every installing project. A component left out is no longer charged.
          items:
            $ref: '#/components/schemas/ListingPricingComponent'
    PublicListing:
      type: object
      description: A listed entry, as any project reads it.
      properties:
        id:
          type: string
          example: lst_V1StGXR8Z5jdHi6B
        resource_type:
          $ref: '#/components/schemas/ListingResourceType'
        resource_id:
          type: string
          description: The id an installing project names the resource by.
          example: tool_V1StGXR8Z5jdHi6B
        title:
          type: string
          example: Invoice OCR
        description:
          type: string
          nullable: true
          example: Reads a scanned invoice and returns its fields.
        pricing:
          type: array
          description: What an installing project is charged per call. Empty is free.
          items:
            $ref: '#/components/schemas/ListingPricingComponent'
        pricing_next:
          $ref: '#/components/schemas/ListingPricingNext'
        interface:
          type: object
          nullable: true
          additionalProperties: true
          description: >
            What an installing project sees of the resource: `id`, `name`,
            `description` and `parameters` for a tool, `id` and `name` for an
            agent. `null` when it cannot be read right now.
        created_at:
          type: string
          format: date-time
          example: '2026-10-01T00:00:00.000Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-10-01T00:00:00.000Z'
      required:
        - id
        - resource_type
        - resource_id
        - title
        - description
        - pricing
        - pricing_next
        - interface
        - created_at
        - updated_at
    PublicListingList:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicListing'
        next_cursor:
          type: string
          nullable: true
          description: Cursor for the next page, or null when there are no more.
          example: null
    ListingInstall:
      type: object
      description: One project's install, as the publisher reads it.
      properties:
        id:
          type: string
          example: ins_V1StGXR8Z5jdHi6B
        project_id:
          type: string
          description: The installing project.
          example: proj_6NgFz3xXy2kPq9Wd
        project_name:
          type: string
          nullable: true
          example: Acme support
        state:
          type: string
          enum: [active, suspended, blocked]
          example: active
        installed_at:
          type: string
          format: date-time
          example: '2026-10-01T00:00:00.000Z'
        calls_cycle:
          type: integer
          nullable: true
          description: Calls through the listing this billing cycle; null before the first sample.
          example: 1280
        errors_cycle:
          type: integer
          nullable: true
          description: Failed tool calls this billing cycle; null for an agent or before the first sample.
          example: 3
        sampled_at:
          type: string
          format: date-time
          nullable: true
          description: When the two counts were last sampled.
          example: '2026-10-01T00:15:00.000Z'
      required:
        - id
        - project_id
        - project_name
        - state
        - installed_at
        - calls_cycle
        - errors_cycle
        - sampled_at
    ListingInstallList:
      type: object
      required: [data, next_cursor]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ListingInstall'
        next_cursor:
          type: string
          nullable: true
          description: Cursor for the next page, or null when there are no more.
          example: null
