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:
| Route | Method | Purpose |
|---|---|---|
/v1/models | GET | Return the public model directory |
/v1/services | GET | Return the registered service list |
/v1/invoke | POST | Generic service invocation |
/v1/text | POST | Text service |
/v1/stream | POST | Streaming text service |
/v1/image | POST | Image service |
/v1/video | POST | Video service |
/v1/tts | POST | TTS service |
/v1/asr | POST | ASR service |
Admin routes
These routes are for AdminClient or other trusted environments. They all require admin_secret_key:
| Route | Method | Purpose |
|---|---|---|
/api/admin/products | GET | Return the product list |
/api/admin/products/create | POST | Create a product |
/api/admin/products/pause | POST | Pause a product |
/api/admin/products/activate | POST | Activate a product |
/api/admin/products/remove | POST | Remove a product |
/api/admin/tokens/apply | POST | Issue user_token |
/api/admin/env | GET | List runtime env |
/api/admin/env/upsert | POST | Write one runtime env item |
/api/admin/env/remove | POST | Delete one env item |
/api/admin/env/import | POST | Import raw .env text |
Product-side request format
Auth header
Authorization: Bearer <user_token>
Content-Type: application/jsonGET /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/jsonPOST /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.
| Status | When it is returned |
|---|---|
200 | The request succeeded, or the service handler returned JSON normally. |
400 | /v1/invoke is missing service in the request body. |
401 | User 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. |
403 | Request body product_id does not match the token product; the token product is paused; a token is being issued for a paused product. |
404 | HTTP route is not found; the service is not registered; a token is being issued for an unknown product. |
422 | The service has no final query.model; the requested model does not exist or is not active; no match() handler matched the current request. |
500 | Required 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;