Files
sim/apps/docs/openapi-v2-billing.json
T
Waleed 29853fbbcf improvement(docs): make the API reference read as code and unify its type token (#6653)
* improvement(docs): make the API reference read as code, and unify the type token

The API-page font override matched every span/div/p inside the page, which
outranks the .font-mono class on specificity, so every parameter name, type,
and identifier silently rendered in the body sans face. Exclude .font-mono so
code tokens stay monospace.

Consolidate the three divergent type-slot treatments — plain scalar, union,
and schema reference each carried their own chip definition, differing in
size, weight, face, and box height — onto one code token that reuses the docs
inline-code recipe and the platform's 20px chip height.

Demote the row metadata: 'required' and 'header' were filled pills, 'required'
on the error token, making a constraint the loudest element on the page and a
page of required parameters read as a page of alarms. Both are now uncontained
text, leaving the type token as the only box on the row.

Pin the two 'application/json' labels to one treatment; the Request Body and
Response headers rendered the same string at different weights and faces.

* improvement(docs): mono status-code tabs, and match fumadocs' lucide icons to emcn

Status codes in the example panel are numeric literals and render as code
everywhere else on the page, including the Response header's own trigger, but
fumadocs rendered the strip in the body sans face. Language tabs sit in a
separate container and stay sans — those are product names, not code.

fumadocs draws a few lucide glyphs on API pages that its client-component
overrides do not expose (the heading anchor and the code-block copy button).
emcn strokes at 1.55 and lucide at 2, so those icons read heavier than every
icon around them; match the weight.

* fix(docs): align the auth type chip with every other property row, and wrap example code

The auth row collapses its real `<token>` type and renders the chip through
::after, so the span is only a wrapper — but it still matched the type-token
rule and kept that rule's border, height, and gap. The border drew a second
empty box around the real chip and the gap opened in front of it, because the
collapsed text remains an anonymous flex item; together they pushed the chip
right by roughly 8px that no other row had.

Example-panel code overflowed sideways instead of wrapping: fumadocs sizes the
block with `w-max`, so it grew to its longest line inside a 400px scroller and
the existing pre-wrap never applied. Cap the width, switch break-all to
overflow-wrap anywhere so only unfittable tokens split, and reserve room for
the copy button fumadocs floats over the first line.

* revert(docs): let example code overflow instead of wrapping

Wrapping restarts every continuation line at column zero, and in a JSON body
indentation is what carries nesting depth — so a wrapped response misreports
its own structure. A hanging indent keeps the depth but needs the shiki lines
forced from flex rows to blocks, which breaks the line rhythm.

Removes the pre-wrap rules rather than repointing them: fumadocs sizes the
block with w-max, so the previous rule never took effect and overflow was
already the behaviour on the page.

* fix(docs): tighten array type tokens and keep the union separator legible

An `array<T>` slot holds its angle brackets as bare text nodes, which become
anonymous flex items, so the slot's gap prised `array<` and `>` away from the
type they wrap. Drop the gap and let the union separator carry its own margin;
this also makes the auth row's gap override redundant.

The separator was dimmed twice, by a muted token and again by opacity, which
on the dark chip fill left `string | null` reading as `string null`.

* fix(docs): restore the hidden API key description, and drop dead API-reference CSS

The rule hiding the trailing `In: header` line matched `p:has(> code)`, which
is a shape, not a target — every scheme description in our specs cites a status
code, so the whole explanation of personal vs workspace-scoped keys was
display:none on every API reference page. Match the last child instead, and
shorten the description to one line now that it renders.

The dropdown trigger's hover rule had been left below a new id-qualified base
rule that outranked it, so the trigger could no longer change colour on hover.

Removes what does not run: the four `::-webkit-scrollbar` rules (specifying a
non-auto scrollbar-width makes Chromium ignore them, and Firefox never had
them) and an `order: 2` block whose selectors and declaration the type-token
rule above it already carried.

Names the two values the API reference repeats — the monospace stack, written
out eleven times, and the 12.5px code size, written nine — as --font-mono-stack
and --text-code. Also drops four !important declarations that already won on
specificity, a --text-muted fallback that can never fire, and a lucide selector
subsumed by the one beside it.

* refactor(docs): define the API-reference chrome once, and cut the commentary

The metadata face — size, leading, weight, mono stack — was written out in seven
rules that a comment asked future readers to keep in sync by hand; it is now one
rule those seven consume, each adding only its own colour, content, and order.
The auth row's chip likewise re-derived all eleven declarations of the type
token and now joins that rule, keeping only its label.

Comments were running longer than the rules they documented — 88 added comment
lines against 73 declarations. Trimmed to the load-bearing facts: cascade traps,
browser behaviour, and the bugs a rule prevents. Dropped the block narrating why
the wrap rules were reverted, which duplicated its own commit message.

Also retires a scrollbar token left unreferenced by the webkit removal, moves
the last two fumadocs colours in our own components onto platform tokens, and
brings the callout icon to 1.55 so the docs really do have one icon weight.

* fix(docs): keep the union separator in the type token's own face

The `|` between union members is a classless span, so the page-wide
`span:not(.font-mono)` rule assigned it the body sans face while the members
beside it stayed mono — one chip rendering in two faces.

Applies the inherit reset to every descendant of a type token rather than just
its links, so anything fumadocs nests there later is covered too.
2026-08-12 19:27:32 -07:00

785 lines
28 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"openapi": "3.1.0",
"info": {
"title": "Sim API v2 — Billing",
"description": "Version 2 of the Sim REST API for billing standing, credit allowance, storage quota, and cursor-paginated usage history.",
"version": "2.0.0",
"contact": {
"name": "Sim Support",
"email": "help@sim.ai",
"url": "https://www.sim.ai"
},
"license": {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"servers": [
{
"url": "https://www.sim.ai",
"description": "Production"
}
],
"tags": [
{
"name": "Billing",
"description": "Inspect billing standing, credit allowance, storage quota, and usage history."
}
],
"security": [
{
"apiKey": []
}
],
"paths": {
"/api/v2/billing/status": {
"get": {
"operationId": "getBillingStatus",
"summary": "Get Billing Status",
"description": "Return the current plan, billing standing, credit allowance, and storage quota. `credits` and `storage` report the payer's pooled allowances and are null unless the caller can manage that payer's billing; they are always null for a workspace API key. Billing history lives at `GET /api/v2/billing/logs`. Without a Stripe subscription — notably on the free plan — there is no real billing period: `period` is the open interval 1970-01-01 to 9999-12-31 and `credits.used` is lifetime consumption, not consumption since a period start.",
"tags": ["Billing"],
"parameters": [
{
"name": "workspaceId",
"in": "query",
"required": false,
"description": "Workspace whose payer should be resolved. Workspace API keys are pinned to their own workspace.",
"schema": {
"description": "Workspace whose payer should be resolved. Workspace API keys are pinned to their own workspace.",
"type": "string",
"minLength": 1
}
}
],
"responses": {
"200": {
"description": "The current billing and storage status.",
"headers": {
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2BillingStatusResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"$ref": "#/components/responses/ServiceUnavailable"
}
}
}
},
"/api/v2/billing/logs": {
"get": {
"operationId": "listBillingLogs",
"summary": "List Billing Logs",
"description": "List the credit-denominated billing ledger with source filtering and opaque cursor pagination. `period` defaults to `30d`, so an unqualified request covers only the last 30 days: paginating to `nextCursor: null` exhausts that window, not the whole ledger. Pass `period=all` for full history, or `period=custom` with `startDate` and `endDate` for a specific range.",
"tags": ["Billing"],
"parameters": [
{
"name": "source",
"in": "query",
"required": false,
"description": "Restrict results to one usage source.",
"schema": {
"description": "Restrict results to one usage source.",
"type": "string",
"enum": [
"workflow",
"wand",
"sim-chat",
"mcp_copilot",
"mothership_block",
"knowledge-base",
"voice-input",
"enrichment",
"voice-output"
]
}
},
{
"name": "workspaceId",
"in": "query",
"required": false,
"description": "Restrict results to one workspace whose payer the caller can inspect.",
"schema": {
"description": "Restrict results to one workspace whose payer the caller can inspect.",
"type": "string",
"minLength": 1
}
},
{
"name": "period",
"in": "query",
"required": false,
"description": "Relative window, all history, or a custom date range.",
"schema": {
"default": "30d",
"description": "Relative window, all history, or a custom date range.",
"type": "string",
"enum": ["1d", "7d", "30d", "all", "custom"]
}
},
{
"name": "startDate",
"in": "query",
"required": false,
"description": "Start of a custom window as a Date-parseable string.",
"schema": {
"description": "Start of a custom window as a Date-parseable string.",
"type": "string",
"minLength": 1
}
},
{
"name": "endDate",
"in": "query",
"required": false,
"description": "End of a custom window as a Date-parseable string; defaults to now.",
"schema": {
"description": "End of a custom window as a Date-parseable string; defaults to now.",
"type": "string",
"minLength": 1
}
},
{
"name": "limit",
"in": "query",
"required": false,
"description": "Maximum usage events per page. Must be a whole number from 1 to 100. Defaults to 50.",
"schema": {
"default": 50,
"description": "Maximum usage events per page. Must be a whole number from 1 to 100. Defaults to 50.",
"type": "integer",
"minimum": 1,
"maximum": 100
}
},
{
"name": "cursor",
"in": "query",
"required": false,
"description": "Opaque cursor returned by the previous page.",
"schema": {
"description": "Opaque cursor returned by the previous page.",
"type": "string",
"minLength": 1
}
}
],
"responses": {
"200": {
"description": "A page of usage events.",
"headers": {
"X-RateLimit-Limit": {
"$ref": "#/components/headers/X-RateLimit-Limit"
},
"X-RateLimit-Remaining": {
"$ref": "#/components/headers/X-RateLimit-Remaining"
},
"X-RateLimit-Reset": {
"$ref": "#/components/headers/X-RateLimit-Reset"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2BillingLogListResponse"
}
}
}
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"429": {
"$ref": "#/components/responses/RateLimited"
},
"500": {
"$ref": "#/components/responses/InternalError"
},
"503": {
"$ref": "#/components/responses/ServiceUnavailable"
}
}
}
}
},
"components": {
"securitySchemes": {
"apiKey": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings > API Keys. Operations that reject workspace keys say so in their own description."
}
},
"headers": {
"X-RateLimit-Limit": {
"description": "Maximum requests allowed in the current window.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"title": "Rate limit",
"description": "Maximum requests allowed in the current window."
}
},
"X-RateLimit-Remaining": {
"description": "Requests remaining in the current window.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"title": "Rate limit remaining",
"description": "Requests remaining in the current window."
}
},
"X-RateLimit-Reset": {
"description": "ISO 8601 timestamp when the current rate-limit window resets.",
"schema": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"title": "Rate limit reset",
"description": "ISO 8601 timestamp when the current rate-limit window resets."
}
},
"Retry-After": {
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset.",
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"title": "Retry after",
"description": "Seconds to wait before retrying. Sent on `429` (derived from the caller rate-limit window) and on `503` (a fixed transient-failure floor). Add jitter rather than retrying at exactly this offset."
}
},
"X-Run-Id": {
"description": "Identifier assigned to the workflow run.",
"schema": {
"type": "string",
"minLength": 1,
"title": "Run identifier",
"description": "Identifier assigned to the workflow run."
}
}
},
"responses": {
"BadRequest": {
"description": "The request is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"Unauthorized": {
"description": "The API key is missing or invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"UsageLimitExceeded": {
"description": "The workspace has exceeded its usage or billing limits.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"Forbidden": {
"description": "The caller lacks the rights this operation requires. Where the cause is one a caller can act on, `error.details.code` names it, drawn from a closed set:\n- `INSUFFICIENT_WORKSPACE_ROLE` — The caller has access to the workspace but its role is below the one this operation requires.\n- `PERSONAL_API_KEYS_DISABLED` — The workspace's organization does not allow personal API keys. Use a workspace API key.\n- `WORKSPACE_KEY_OPERATION_NOT_PERMITTED` — This operation is not available to a workspace-scoped API key. Use a personal API key.\n- `PRINCIPAL_KIND_NOT_PERMITTED` — This operation does not accept the caller’s kind of API key.\n- `ORGANIZATION_MEMBERSHIP_REQUIRED` — The caller is not a member of the organization it named.\n- `ORGANIZATION_ADMIN_REQUIRED` — The caller is a member of the organization but not an admin or owner.\n- `ENTERPRISE_PLAN_REQUIRED` — The organization has no active enterprise subscription.\n- `AUDIT_LOGS_DISABLED` — Audit logging is not enabled for this deployment.\n- `SKILL_EDITOR_ACCESS_REQUIRED` — The caller can write in the workspace but is not an editor of this skill.\n- `MCP_SERVER_URL_NOT_ALLOWED` — The supplied MCP server URL is outside the allowed domains or resolves to an internal address.\nA resource in a workspace the caller cannot reach at all answers `404`, not `403`, so absence and denial are indistinguishable to a caller who was never entitled to tell them apart.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"NotFound": {
"description": "The requested resource was not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"Conflict": {
"description": "The request conflicts with current resource state.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"RunIdConflict": {
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
"headers": {
"X-Run-Id": {
"$ref": "#/components/headers/X-Run-Id"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"Gone": {
"description": "The requested generated resource has expired.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"PayloadTooLarge": {
"description": "The request, or a resource collection it must materialize, exceeds the allowed size. Besides an oversized request body, this covers a generated artifact that renders past the download ceiling and a workspace folder tree too large to load in full.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"UnsupportedMediaType": {
"description": "The request uses an unsupported media type.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"Locked": {
"description": "The resource is locked and cannot be modified.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"RateLimited": {
"description": "The caller exceeded the request rate limit.",
"headers": {
"Retry-After": {
"$ref": "#/components/headers/Retry-After"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"ClientClosedRequest": {
"description": "The client closed the connection before the response was produced.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"InternalError": {
"description": "An unexpected server error occurred.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
},
"ServiceUnavailable": {
"description": "A required service is temporarily unavailable. The condition is transient, so the response normally carries `Retry-After` with the number of seconds to wait; treat that value as a floor and add jitter before retrying. One case deliberately omits the header: when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, the run may already have started, so retrying could start and bill a second run. Reconcile against the returned run id instead of retrying.",
"headers": {
"Retry-After": {
"$ref": "#/components/headers/Retry-After"
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/V2Error"
}
}
}
}
},
"schemas": {
"V2Error": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Stable machine-readable error code."
},
"message": {
"type": "string",
"description": "Human-readable explanation of the error."
},
"details": {
"description": "Optional structured error details."
}
},
"required": ["code", "message"],
"additionalProperties": false,
"description": "Canonical error details."
}
},
"required": ["error"],
"additionalProperties": false,
"title": "v2 error response",
"description": "Canonical error envelope returned by the public v2 API.",
"examples": [
{
"error": {
"code": "BAD_REQUEST",
"message": "The request is invalid."
}
}
]
},
"V2BillingStatus": {
"type": "object",
"properties": {
"workspaceId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workspace whose payer was resolved, or null for account billing."
},
"period": {
"type": "object",
"properties": {
"start": {
"type": "string",
"description": "ISO 8601 start of the current billing period, or 1970-01-01T00:00:00.000Z when no Stripe subscription defines one.",
"format": "date-time"
},
"end": {
"type": "string",
"description": "ISO 8601 end of the current billing period, or 9999-12-31T00:00:00.000Z when no Stripe subscription defines one.",
"format": "date-time"
}
},
"required": ["start", "end"],
"additionalProperties": false,
"description": "Current billing period. Only a Stripe subscription defines a real period; without one — notably on the free plan — this is the open interval 1970-01-01 to 9999-12-31 and must not be read as a monthly window."
},
"plan": {
"type": "string",
"description": "Current billing plan."
},
"status": {
"type": "string",
"enum": ["active", "limit_exceeded", "billing_blocked"],
"description": "Current billing standing."
},
"credits": {
"anyOf": [
{
"type": "object",
"properties": {
"used": {
"type": "number",
"description": "Credits consumed so far. The counter is reset by Stripe invoice webhooks, so on a paid plan it covers the current billing period; on the free plan nothing resets it and the value is lifetime consumption."
},
"limit": {
"type": "number",
"description": "Credit allowance for the reporting window — per billing period on a paid plan, lifetime on the free plan."
},
"remaining": {
"type": "number",
"description": "Allowance minus consumption, over the same window."
}
},
"required": ["used", "limit", "remaining"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The payer's credit usage and allowance — periodic on a paid plan, lifetime on the free plan, where the counter never resets. Null when the caller cannot manage that payer's billing. Always null for a workspace API key."
},
"storage": {
"anyOf": [
{
"type": "object",
"properties": {
"usedBytes": {
"type": "number",
"minimum": 0,
"description": "Storage currently consumed, in bytes."
},
"limitBytes": {
"type": "number",
"minimum": 0,
"description": "Storage quota, in bytes."
},
"percentUsed": {
"type": "number",
"minimum": 0,
"description": "Percentage of the storage quota consumed."
}
},
"required": ["usedBytes", "limitBytes", "percentUsed"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The payer's storage consumption and quota, or null when the caller cannot manage that payer's billing. Always null for a workspace API key."
}
},
"required": ["workspaceId", "period", "plan", "status", "credits", "storage"],
"additionalProperties": false,
"title": "Billing status",
"description": "Current billing standing, credit allowance, and storage quota."
},
"V2BillingStatusResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
"$ref": "#/components/schemas/V2BillingStatus"
}
},
"required": ["data"],
"additionalProperties": false,
"title": "Billing status response",
"description": "Current billing standing, credit allowance, and storage quota.",
"examples": [
{
"data": {
"workspaceId": null,
"period": {
"start": "2026-07-01T00:00:00.000Z",
"end": "2026-08-01T00:00:00.000Z"
},
"plan": "pro",
"status": "active",
"credits": {
"used": 512,
"limit": 20000,
"remaining": 19488
},
"storage": {
"usedBytes": 5242880,
"limitBytes": 1073741824,
"percentUsed": 0.48828125
}
}
}
]
},
"V2BillingLogEntry": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique usage-event identifier."
},
"createdAt": {
"type": "string",
"description": "ISO 8601 timestamp when the usage event was recorded.",
"format": "date-time"
},
"source": {
"type": "string",
"enum": [
"workflow",
"wand",
"sim-chat",
"mcp_copilot",
"mothership_block",
"knowledge-base",
"voice-input",
"enrichment",
"voice-output"
],
"description": "Product surface that consumed the credits."
},
"workspaceId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workspace attributed to the event, or null for account-level usage."
},
"workflow": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Workflow identifier."
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow display name, when available."
}
},
"required": ["id", "name"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "Workflow attributed to the event, when applicable."
},
"runId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Workflow run attributed to the event, when applicable."
},
"creditCost": {
"type": "number",
"description": "Credits apportioned to the event so page rows sum to the rounded page total; may be zero for a sub-credit event."
}
},
"required": ["id", "createdAt", "source", "workspaceId", "workflow", "runId", "creditCost"],
"additionalProperties": false,
"title": "Billing log entry",
"description": "One credit-consuming usage event in the billing ledger."
},
"V2BillingLogListResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/V2BillingLogEntry"
},
"description": "Items in the current page."
},
"nextCursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Opaque cursor for the next page: send it back as `cursor` to continue, and stop when it is null. Most v2 lists page, so null means the last page was reached. A few are full-set lists that return their whole bounded result in one response and therefore always report null; those say so in the operation description. Either way, null means there is nothing further to fetch — never construct a cursor yourself."
}
},
"required": ["data", "nextCursor"],
"additionalProperties": false,
"title": "Billing log list response",
"description": "A cursor-paginated page of credit-consuming usage events.",
"examples": [
{
"data": [
{
"id": "log_1",
"createdAt": "2026-07-29T18:04:11.000Z",
"source": "sim-chat",
"workspaceId": "ws_1",
"workflow": null,
"runId": null,
"creditCost": 12
}
],
"nextCursor": null
}
]
}
}
},
"x-generated-by": "scripts/generate-openapi.ts"
}