{
  "openapi": "3.0.1",
  "info": {
    "title": "Nubo Public API",
    "description": "Read-only integration API for tenant-owned systems.\n\nAuthenticate with your tenant's API key on the `X-API-Key` header. Keys are issued and revoked from the Developer page in the Nubo portal; the full key is shown once, at creation, and cannot be retrieved again.\n\nMost endpoints accept an optional `projectId` query parameter. Omit it to span every project the key's tenant owns.\n\nErrors share one shape: `{ \"error\": { \"code\", \"message\", \"traceId\" } }`. Branch on `code` — it is a stable contract; `message` is free text and may change.\n\nThe first request after a period of inactivity can take up to a minute while the underlying database resumes from its idle state. Retry rather than treating it as an outage.",
    "version": "v1"
  },
  "paths": {
    "/v1/balance": {
      "get": {
        "tags": [
          "Balance"
        ],
        "summary": "Returns the tenant's reply allowance, consumption and remaining balance for the\r\ncurrent period.",
        "description": "This endpoint does <strong>not</strong> accept `?projectId=`: the reply allowance\r\nis a single tenant-wide pool shared by every project, so a per-project balance would\r\nnot be a meaningful number.\r\n\n\r\nThe figure is an approximation of a billing statement, not a substitute for one. Read\r\n`periodStart` / `periodEnd` from the response rather than assuming calendar\r\nmonth boundaries — see `periodBasis`.\r\n",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BalanceResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/plan": {
      "get": {
        "tags": [
          "Plan"
        ],
        "summary": "Returns the plan currently assigned to this API key's tenant.",
        "description": "Entitlements describe what the plan grants, never what it costs — no pricing or billing\r\ndata is exposed here. Plan keys are stable; display names are not.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanResponse"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Lists the tenant's projects.",
        "parameters": [
          {
            "name": "projectId",
            "in": "query",
            "description": "Optional. Narrows the result to a single project. Omit for all of them.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProjectDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{id}": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Fetches a single project by id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectDto"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "The project does not exist, or does not belong to this API key's tenant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sessions": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "Lists sessions, most recently active first.",
        "description": "Offset paging drifts: a poller walking pages while new sessions arrive can skip rows\r\nas later pages shift underneath it. If you are polling, <strong>paginate inside a\r\nclosed `from`/`to` window</strong> — a window that is no longer receiving new\r\nsessions cannot shift, which removes the drift entirely.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Optional lower bound on activity (UTC, inclusive).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Optional upper bound on activity (UTC, inclusive).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Rows per page, capped at 100.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 25
            }
          },
          {
            "name": "projectId",
            "in": "query",
            "description": "Optional. Restricts the list to one project.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sessions/{id}": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "Fetches one session's aggregates and its per-turn metadata, oldest turn first.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionDetail"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "The session does not exist under this API key's tenant (or the given project).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Usage"
        ],
        "summary": "Headline totals over a trailing window: replies, tokens, average duration and tool calls.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "Size of the trailing window, 1–365. Defaults to 30.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 30
            }
          },
          {
            "name": "projectId",
            "in": "query",
            "description": "Optional. Restricts the aggregate to one project.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatUsageSummary"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage/series": {
      "get": {
        "tags": [
          "Usage"
        ],
        "summary": "One data point per UTC day over the window. Days with no activity are absent, not zero-filled.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 30
            }
          },
          {
            "name": "projectId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ChatUsageTimePoint"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage/models": {
      "get": {
        "tags": [
          "Usage"
        ],
        "summary": "Usage broken down by the model that served each reply, busiest first.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 30
            }
          },
          {
            "name": "projectId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ModelUsageItem"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "BalanceResponse": {
        "type": "object",
        "properties": {
          "planKey": {
            "type": "string",
            "description": "The tenant's current plan key, e.g. `free`, `starter`, `pro`.",
            "nullable": true
          },
          "included": {
            "type": "integer",
            "description": "Replies included in the plan for one period. `null` means unlimited.",
            "format": "int32",
            "nullable": true
          },
          "used": {
            "type": "integer",
            "description": "Billable replies recorded for this tenant in the current period, across all its projects.",
            "format": "int64"
          },
          "remaining": {
            "type": "integer",
            "description": "`included - used`, floored at zero. `null` when Nubo.PublicApi.Contracts.BalanceResponse.Included is\r\n            null (an unlimited plan has no remaining count to report).",
            "format": "int64",
            "nullable": true
          },
          "periodStart": {
            "type": "string",
            "description": "Start of the current period, inclusive (UTC).",
            "format": "date-time"
          },
          "periodEnd": {
            "type": "string",
            "description": "End of the current period, exclusive (UTC).",
            "format": "date-time"
          },
          "periodBasis": {
            "type": "string",
            "description": "How the period was derived. Reads `calendar_month` today; when real subscription\r\nperiods land it becomes `subscription_period` and the dates change meaning while\r\nthis contract does not.\r\n\n<strong>Do not hardcode month boundaries.</strong> Read\r\nNubo.PublicApi.Contracts.BalanceResponse.PeriodStart/Nubo.PublicApi.Contracts.BalanceResponse.PeriodEnd from the response instead — an\r\nintegration that assumes the 1st of the month will silently drift when this changes.\r\n",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Remaining billable AI replies in the current period.\r\n\n\r\nA <em>computed projection</em>, not a stored quantity: there is no wallet, no credits and\r\nno money behind it. It is the tenant's plan allowance minus the replies recorded against\r\nthe tenant in the period.\r\n"
      },
      "ChatUsageSummary": {
        "type": "object",
        "properties": {
          "rangeDays": {
            "type": "integer",
            "format": "int32"
          },
          "totalConversations": {
            "type": "integer",
            "format": "int32"
          },
          "totalMessages": {
            "type": "integer",
            "format": "int32"
          },
          "totalTokens": {
            "type": "integer",
            "format": "int64"
          },
          "promptTokens": {
            "type": "integer",
            "format": "int64"
          },
          "completionTokens": {
            "type": "integer",
            "format": "int64"
          },
          "avgResponseMs": {
            "type": "integer",
            "format": "int32"
          },
          "rerankingRate": {
            "type": "number",
            "format": "double"
          },
          "toolCalls": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "ChatUsageTimePoint": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "messages": {
            "type": "integer",
            "format": "int32"
          },
          "conversations": {
            "type": "integer",
            "format": "int32"
          },
          "tokens": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "FeaturesEntitlementDto": {
        "type": "object",
        "properties": {
          "reranking": {
            "type": "boolean"
          },
          "mcpTools": {
            "type": "boolean"
          },
          "maxToolInvocations": {
            "type": "integer",
            "format": "int32"
          },
          "removeBranding": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "KnowledgeEntitlementDto": {
        "type": "object",
        "properties": {
          "maxSources": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ModelUsageItem": {
        "type": "object",
        "properties": {
          "modelName": {
            "type": "string",
            "nullable": true
          },
          "provider": {
            "type": "string",
            "nullable": true
          },
          "messages": {
            "type": "integer",
            "format": "int32"
          },
          "tokens": {
            "type": "integer",
            "format": "int64"
          }
        },
        "additionalProperties": false
      },
      "ModelsEntitlementDto": {
        "type": "object",
        "properties": {
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "autoSelect": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "PlanEntitlements": {
        "type": "object",
        "properties": {
          "schemaVersion": {
            "type": "integer",
            "format": "int32"
          },
          "replyAllowance": {
            "$ref": "#/components/schemas/ReplyAllowanceDto"
          },
          "rateLimits": {
            "$ref": "#/components/schemas/RateLimitsDto"
          },
          "models": {
            "$ref": "#/components/schemas/ModelsEntitlementDto"
          },
          "features": {
            "$ref": "#/components/schemas/FeaturesEntitlementDto"
          },
          "knowledge": {
            "$ref": "#/components/schemas/KnowledgeEntitlementDto"
          },
          "projects": {
            "$ref": "#/components/schemas/ProjectsEntitlementDto"
          }
        },
        "additionalProperties": false
      },
      "PlanResponse": {
        "type": "object",
        "properties": {
          "planKey": {
            "type": "string",
            "description": "Stable plan identifier, e.g. `free`, `starter`, `pro`.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Human-readable plan name, for display. May change without notice.",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "entitlements": {
            "$ref": "#/components/schemas/PlanEntitlements"
          }
        },
        "additionalProperties": false,
        "description": "The tenant's plan and what it entitles them to."
      },
      "ProjectDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "isActive": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "isDefault": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "ProjectsEntitlementDto": {
        "type": "object",
        "properties": {
          "max": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiErrorBody": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "A stable code from Nubo.PublicApi.Errors.PublicApiErrorCodes. Safe to branch on.",
            "nullable": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable and free to change — never branch on this.",
            "nullable": true
          },
          "traceId": {
            "type": "string",
            "description": "Correlates the response with the server-side log entry. Quote it in support requests.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PublicApiErrorEnvelope": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/PublicApiErrorBody"
          }
        },
        "additionalProperties": false,
        "description": "The single error shape every non-2xx response on `/v1` uses:\r\n```\r\n{ \"error\": { \"code\": \"invalid_api_key\", \"message\": \"…\", \"traceId\": \"0HN7…\" } }\r\n```"
      },
      "RateLimitsDto": {
        "type": "object",
        "properties": {
          "requestsPerMinute": {
            "type": "integer",
            "format": "int32"
          },
          "maxConcurrent": {
            "type": "integer",
            "format": "int32"
          },
          "sessionRequestsPerMinute": {
            "type": "integer",
            "format": "int32"
          },
          "ipRequestsPerMinute": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "ReplyAllowanceDto": {
        "type": "object",
        "properties": {
          "included": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "period": {
            "type": "string",
            "nullable": true
          },
          "overage": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SessionDetail": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "nullable": true
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastActivityAt": {
            "type": "string",
            "format": "date-time"
          },
          "messages": {
            "type": "integer",
            "format": "int32"
          },
          "totalTokens": {
            "type": "integer",
            "format": "int64"
          },
          "avgResponseMs": {
            "type": "integer",
            "format": "int32"
          },
          "toolCalls": {
            "type": "integer",
            "format": "int32"
          },
          "rerankingRate": {
            "type": "number",
            "format": "double"
          },
          "models": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "turns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SessionTurn"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SessionListItem": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "nullable": true
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastActivityAt": {
            "type": "string",
            "format": "date-time"
          },
          "messages": {
            "type": "integer",
            "format": "int32"
          },
          "totalTokens": {
            "type": "integer",
            "format": "int64"
          },
          "avgResponseMs": {
            "type": "integer",
            "format": "int32"
          },
          "toolCalls": {
            "type": "integer",
            "format": "int32"
          },
          "rerankingRate": {
            "type": "number",
            "format": "double"
          },
          "models": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SessionListResponse": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SessionListItem"
            },
            "nullable": true
          },
          "total": {
            "type": "integer",
            "format": "int32"
          },
          "page": {
            "type": "integer",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "SessionTurn": {
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "modelName": {
            "type": "string",
            "nullable": true
          },
          "provider": {
            "type": "string",
            "nullable": true
          },
          "promptTokens": {
            "type": "integer",
            "format": "int32"
          },
          "completionTokens": {
            "type": "integer",
            "format": "int32"
          },
          "totalTokens": {
            "type": "integer",
            "format": "int32",
            "readOnly": true
          },
          "durationMs": {
            "type": "integer",
            "format": "int32"
          },
          "toolCalls": {
            "type": "integer",
            "format": "int32"
          },
          "rerankingApplied": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "Your tenant's Nubo API key, e.g. `nubo_…`.",
        "name": "X-API-Key",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "ApiKey": [ ]
    }
  ],
  "tags": [
    {
      "name": "Balance",
      "description": "Remaining billable replies for the tenant in the current period."
    },
    {
      "name": "Plan",
      "description": "The tenant's plan and its entitlements."
    },
    {
      "name": "Projects",
      "description": "The chatbots owned by the tenant this API key belongs to."
    },
    {
      "name": "Sessions",
      "description": "Conversations between an end visitor and a project's chatbot, reconstructed by grouping\r\nrecorded turns under a session id."
    },
    {
      "name": "Usage",
      "description": "Aggregated chat usage for the tenant. Every endpoint accepts an optional\r\n`?projectId=`; omitting it aggregates across all of the tenant's projects."
    }
  ]
}