VisibleBase
Reference

HTTP API

Auth boundaries, request formats, and response shapes for `/v1/*` and `/api/admin/*`.

If you already use the SDK, you usually do not need to write raw HTTP calls.

This page is more useful when:

  • you are integrating another runtime
  • you are debugging the actual requests sent to Base
  • you need to confirm what a route really expects and returns

Route groups

Product-side routes

These routes are for UserClient or product clients. They all require user_token:

RouteMethodPurpose
/v1/modelsGETReturn the public model directory
/v1/servicesGETReturn the registered service list
/v1/invokePOSTGeneric service invocation
/v1/textPOSTText service
/v1/streamPOSTStreaming text service
/v1/imagePOSTImage service
/v1/videoPOSTVideo service
/v1/ttsPOSTTTS service
/v1/asrPOSTASR service

Admin routes

These routes are for AdminClient or other trusted environments. They all require admin_secret_key:

RouteMethodPurpose
/api/admin/productsGETReturn the product list
/api/admin/products/createPOSTCreate a product
/api/admin/products/pausePOSTPause a product
/api/admin/products/activatePOSTActivate a product
/api/admin/products/removePOSTRemove a product
/api/admin/tokens/applyPOSTIssue user_token
/api/admin/envGETList runtime env
/api/admin/env/upsertPOSTWrite one runtime env item
/api/admin/env/removePOSTDelete one env item
/api/admin/env/importPOSTImport raw .env text

Product-side request format

Auth header

Authorization: Bearer <user_token>
Content-Type: application/json

GET /v1/models

[
  {
    "id": "gpt-5.4",
    "name": "GPT-5.4",
    "provider": "openai",
    "description": "Primary text model",
    "primary": true
  }
]

GET /v1/services

{
  "items": ["text", "stream", "image"]
}

POST /v1/text

Request:

{
  "product_id": "prod_xxx",
  "model": "gpt-5.4",
  "prompt": "Write a welcome message"
}

Response:

{
  "id": "msg_xxx",
  "role": "assistant",
  "parts": [
    {
      "type": "text",
      "text": "Welcome to VisibleBase",
      "state": "done"
    }
  ]
}

The exact response body is defined by your own service handler. When using UserClient.text(), prefer returning an AI SDK UIMessage.

POST /v1/invoke

Request:

{
  "service": "rewrite",
  "product_id": "prod_xxx",
  "model": "gpt-5.4",
  "prompt": "Rewrite this in a more professional tone"
}

This differs from POST /v1/text only because the service name is explicitly carried in the body.

POST /v1/stream

The request body is similar to text:

{
  "product_id": "prod_xxx",
  "model": "gpt-5.4",
  "prompt": "Stream a short paragraph"
}

The response is not a normal JSON body. When using UserClient.stream(), the Base-side handler should return an AI SDK UIMessage stream response, and the client parses it into a UIMessageChunk stream.

Admin request format

Auth header

Authorization: Bearer <admin_secret_key>
Content-Type: application/json

POST /api/admin/products/create

Request:

{
  "name": "Chrome Extension"
}

Response:

{
  "product_id": "prod_xxx",
  "name": "Chrome Extension",
  "status": "active",
  "created_at": "2026-04-23T00:00:00.000Z",
  "updated_at": "2026-04-23T00:00:00.000Z"
}

POST /api/admin/tokens/apply

Request:

{
  "product_id": "prod_xxx",
  "user_id": "user_123",
  "metadata": {
    "plan": "pro"
  },
  "ttl": "7d"
}

Response:

{
  "user_token": "ub_xxx",
  "product_id": "prod_xxx",
  "user_id": "user_123",
  "expires_at": "2026-04-30T00:00:00.000Z"
}

POST /api/admin/env/upsert

Request:

{
  "key": "OPENAI_API_KEY",
  "value": "sk-xxx"
}

POST /api/admin/env/remove

Request:

{
  "key": "OPENAI_API_KEY"
}

POST /api/admin/env/import

Request:

{
  "raw": "OPENAI_API_KEY=sk-xxx\nOPENAI_BASE_URL=https://api.openai.com/v1"
}

Common status codes

Errors produced by Base itself use JSON:

{
  "error": "Invalid user token"
}

If a service handler returns a Response, Base passes that Response through unchanged. If a handler throws, Base runs the service-level and global onError hooks, then converts the error into an HTTP response. Errors with statusCode use that status; other errors return 500.

StatusWhen it is returned
200The request succeeded, or the service handler returned JSON normally.
400/v1/invoke is missing service in the request body.
401User request is missing user_token; token expired, has an invalid signature, invalid audience, or invalid payload; the token product no longer exists; admin request is missing or uses the wrong admin_secret_key.
403Request body product_id does not match the token product; the token product is paused; a token is being issued for a paused product.
404HTTP route is not found; the service is not registered; a token is being issued for an unknown product.
422The service has no final query.model; the requested model does not exist or is not active; no match() handler matched the current request.
500Required Base config is missing, such as VISIBLEBASE_TOKEN_SIGNING_KEY or VISIBLEBASE_ADMIN_SECRET_KEY; the service is registered but has no handler; handler, provider, database, or env logic threw an error without an explicit statusCode.

In a custom handler, throw an error with statusCode when you want to control the HTTP status:

import type { ErrorWithStatus } from "@visiblebase/base";

const error = new Error("quota exceeded") as ErrorWithStatus;
error.statusCode = 429;
throw error;

On this page