Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions .vitepress/theme/generatedApiReferenceSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const generatedApiReferenceSpecs: Record<string, any> = {
],
"response": {
"status": "200 OK",
"code": "{\n \"org_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"balance\": \"42.125000\",\n \"credit_limit\": \"10.000000\",\n \"alert_threshold\": \"5.000000\"\n}"
"code": "{\n \"org_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"balance\": \"42.125\",\n \"credit_limit\": \"10\",\n \"alert_threshold\": \"5\"\n}"
}
},
"account/history": {
Expand Down Expand Up @@ -70,7 +70,7 @@ export const generatedApiReferenceSpecs: Record<string, any> = {
"title": "Request body",
"schema": "ImageEditParams",
"fields": [
{ "name": "model", "type": "string", "required": true, "description": "Use gpt-image-2 on this endpoint." },
{ "name": "model", "type": "string", "required": true, "description": "One of gpt-image-2, gpt-image-2.5-flare, or gpt-image-2.5-sunburst." },
{ "name": "prompt", "type": "string", "required": true, "description": "Text description of the requested edit." },
{ "name": "image", "type": "file | file[]", "required": true, "description": "One or more source image files. Repeat the multipart field for multiple images." },
{ "name": "mask", "type": "file", "required": false, "description": "Optional mask image for inpainting." },
Expand Down Expand Up @@ -120,6 +120,15 @@ export const generatedApiReferenceSpecs: Record<string, any> = {
{ "name": "Audio", "type": "string", "required": false, "description": "audio/mpeg, audio/mp4, audio/wav, audio/x-wav, audio/ogg, audio/aac, audio/flac — maximum 50 MB." },
{ "name": "Video", "type": "string", "required": false, "description": "video/mp4, video/quicktime, video/webm — maximum 500 MB." }
]
},
{
"title": "Response",
"fields": [
{ "name": "url", "type": "string", "required": true, "description": "Stored media URL to pass to a supported model input." },
{ "name": "filename · size", "type": "string · integer", "required": true, "description": "Original multipart filename and file size in bytes." },
{ "name": "type · content_type", "type": "string", "required": true, "description": "Detected MIME type used when storing the file." },
{ "name": "duration_seconds", "type": "number", "required": false, "description": "Audio and video only: media duration in seconds, when it can be read from the file." }
]
}
],
"examples": [
Expand All @@ -131,7 +140,7 @@ export const generatedApiReferenceSpecs: Record<string, any> = {
],
"response": {
"status": "200 OK",
"code": "{\n \"url\": \"https://media.sandbase.ai/uploads/2026/08/550e8400-e29b-41d4-a716-446655440000.mp4\",\n \"filename\": \"reference.mp4\",\n \"size\": 1048576,\n \"type\": \"video/mp4\",\n \"content_type\": \"video/mp4\"\n}"
"code": "{\n \"url\": \"https://media.sandbase.ai/uploads/2026/08/550e8400-e29b-41d4-a716-446655440000.mp4\",\n \"filename\": \"reference.mp4\",\n \"size\": 1048576,\n \"type\": \"video/mp4\",\n \"content_type\": \"video/mp4\",\n \"duration_seconds\": 5.04\n}"
},
"notes": [
{ "title": "MIME detection", "description": "The server detects the file type from its bytes and filename. The optional type field does not force a MIME type." }
Expand Down Expand Up @@ -471,7 +480,7 @@ export const generatedApiReferenceSpecs: Record<string, any> = {
"title": "Request body",
"schema": "ImageGenerateParams",
"fields": [
{ "name": "model", "type": "string", "required": true, "description": "Use gpt-image-2 on this endpoint." },
{ "name": "model", "type": "string", "required": true, "description": "One of gpt-image-2, gpt-image-2.5-flare, or gpt-image-2.5-sunburst." },
{ "name": "prompt", "type": "string", "required": true, "description": "Text description of the image to generate." },
{ "name": "n", "type": "integer", "required": false, "description": "Number of images to generate when supported." },
{ "name": "size", "type": "string", "required": false, "description": "Output dimensions supported by the model." },
Expand Down Expand Up @@ -721,6 +730,15 @@ export const generatedApiReferenceSpecs: Record<string, any> = {
"description": "Opaque task ID returned by a billable operation, including an x-task-id response header when present."
}
]
},
{
"title": "Response",
"fields": [
{ "name": "settled", "type": "boolean", "required": true, "description": "Whether final cost settlement has completed. cost is not final until it is true." },
{ "name": "cost · estimated_cost", "type": "string", "required": true, "description": "USD amounts with six decimal places. estimated_cost is a best-effort reference before settlement." },
{ "name": "usage", "type": "object", "required": true, "description": "Token counts recorded for the task." },
{ "name": "pricing", "type": "object", "required": false, "description": "After settlement, when a discount was recorded: original_cost, discount_amount, discount_percent, and optional discount_type. original_cost minus discount_amount equals cost." }
]
}
],
"examples": [
Expand Down
22 changes: 9 additions & 13 deletions admin/rate-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ description: SandBase API rate limiting — how limits are enforced and how to h

# Rate Limits

SandBase enforces rate limits to ensure fair usage and platform stability. Limiting is applied as a 1-minute sliding window using two layers, and a request must pass both to be served:
SandBase enforces rate limits to ensure fair usage and platform stability. The request limit is set per organization: an optional requests-per-minute cap, counted over a 1-minute sliding window and shared by every API key in the organization.

1. **Per-key limit** — an optional per-minute request cap on an individual API key. When unset, the key is only subject to the global limit.
2. **Global limit** — a platform-wide per-minute request cap.
It applies to `POST /v1/run`, `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/messages`, `POST /v1/upload`, and the Gemini `generateContent`, `streamGenerateContent`, and `POST /v1beta/interactions` endpoints. Polling endpoints such as `GET /v1/run/{id}` are not counted.

## How to find your effective limit

SandBase does not publish a single fixed requests-per-minute number because the effective limit can vary by workspace, key, and platform capacity. Your key is governed by the lower of any custom per-key limit and the current platform-wide protection limit.
SandBase does not publish a single fixed requests-per-minute number because the limit is assigned per organization. Every API key in the organization counts toward the same limit, so creating more keys does not raise it.

If your workload needs a guaranteed or higher limit, contact SandBase support with the organization, application, expected steady rate, and expected burst rate. Do not design a production client around an undocumented numeric default.

Expand All @@ -22,11 +21,13 @@ When a limit is exceeded, the API aborts the request with HTTP 429. On the `/v1/

```json
{
"error": "API key rate limit exceeded"
"error": "organization rate limit exceeded"
}
```

The message is `"API key rate limit exceeded"` when the per-key limit is hit, or `"global rate limit exceeded"` when the platform-wide limit is hit.
Protocol-compatible endpoints such as `POST /v1/messages` and the Gemini endpoints return the same `429` in their own error envelope.

API operations that are currently free to call also share a daily call allowance per organization that resets at 00:00 UTC. When it is used up, those calls return `429` with `"daily free API limit exceeded"`; retrying before the reset does not help.

::: warning
Platform-generated rate-limit responses do not currently add `Retry-After` or remaining-quota headers. A compatible
Expand All @@ -42,11 +43,6 @@ client-side backoff with jitter.
4. **Add jitter** — randomize retry delays so multiple workers do not retry at the same moment.
5. **Retry only safe operations** — repeating a generation can add cost, and state-changing requests can duplicate work.

## Per-Key Limit

A per-key request cap can be configured for an individual key. When present, it is enforced independently of and in
addition to the platform-wide limit.
## Changing your limit

::: info
Setting a per-key rate limit is not currently exposed through the public key-management API. Contact support if you need a custom per-key limit configured.
:::
The organization limit is not currently exposed through the public API or Console. Contact support if your workload needs a different limit.
8 changes: 4 additions & 4 deletions agents/openai-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ SandBase runs every Session in its hosted cloud; there is no environment to choo
| Area | Status | Details |
|---|---|---|
| `beta.agents.create / retrieve / update / list / delete` | Compatible, with extensions | Responses add `status`, `version`, `row_version`, `projection_status`, `runtime_profile`, `skills`, `mcp_connections`, and more. `delete` archives the Agent and returns `agent.deleted`. |
| `beta.agents.sessions.create / retrieve / update / list / delete` | Compatible, with limits | Requires a saved `agent_id`; `agent` only accepts `{"model": ...}`. `update` only accepts `metadata`. `delete` archives the Session. |
| `beta.agents.sessions.create / retrieve / update / list / delete` | Compatible, with limits | Accepts a saved `agent_id`, an inline `agent`, or both; `agent` accepts `model`, `instructions`, `runtime_profile`, `service_tier`, `tools`, `text`, `reasoning`, and `multi_agent`. `stream=True` streams the first turn and needs an initial `input`. `update` only accepts `metadata`. `delete` archives the Session. |
| `sessions.events.stream` | Compatible | Live-only, like OpenAI. `Last-Event-ID` or `?after=` returns `400`. |
| `sessions.events.create` | Compatible, with limits | Exactly one `agent.session.input.message` or `agent.session.input.cancel` event per request; the message has one user `input_text` part or a plain string. |
| `sessions.items.list`, `sessions.turns.list / retrieve` | Compatible | Cursor pagination with `limit` 1-100, `after`, `order`. |
Expand All @@ -243,9 +243,9 @@ These OpenAI SDK calls fail against SandBase. The message is the actual `error.m

| SDK call | Result |
|---|---|
| `sessions.create(...)` with `agent` but no `agent_id` (inline Agent) | `400`: `a saved SandBase agent_id and openai_hosted environment are required; standalone inline agents are unsupported` |
| `sessions.create(..., stream=True)` (also `with_result_collection()`) | `400`: `creation streaming is not supported; create the session then subscribe to events before sending input` |
| `sessions.create(..., environment={"type": "self_hosted"})` | `400`, same message as the inline Agent case |
| `sessions.create(..., stream=True)` without an initial `input` | `400 invalid_request` |
| `sessions.create(..., environment={"type": "self_hosted"})` | `400`: `only openai_hosted environment is supported` |
| `sessions.create(..., agent={...})` with a field other than `model`, `instructions`, `runtime_profile`, `service_tier`, `tools`, `text`, `reasoning`, and `multi_agent` | `400 invalid_request` |
| `sessions.create(..., vault_ids=[...])` | `400 invalid_request` |
| `sessions.update(..., agent={...})` | `400` (openai-python 3.13.0 has no `agent` parameter and raises `TypeError`) |
| `agents.create(..., multi_agent={"enabled": True})` | `400`: `multi_agent is not supported` |
Expand Down
11 changes: 8 additions & 3 deletions api-reference/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ API keys are scoped to an organization. When you authenticate with a key:
| 401 | `API key has been revoked` | Key was disabled |
| 401 | `API key has expired` | Key past expiration date |
| 402 or protocol-specific 403 | `API key spending limit exceeded` | Key-level spending limit reached; inspect the endpoint's documented error contract |
| 403 | `insufficient_scope` | Scoped key is not authorized for this endpoint |
| 403 | `API key is not authorized for this endpoint` | Scoped key used on an endpoint outside its scope |

These messages arrive in the flat `{"error":"<message>"}` body, except `403` scope errors. On model and account
endpoints those use `{"error":{"code":"insufficient_scope","message":"API key is not authorized for this endpoint","request_id":"..."}}`;
on Agents platform resources they use the [Agents error envelope](/api-reference/errors#agents-platform-errors) with
`code` `insufficient_scope`.

## Security Best Practices

Expand Down Expand Up @@ -217,8 +222,8 @@ const client = new OpenAI({

## Rate Limiting by Key

An API key may have an optional per-key RPM cap. Every request is also subject to the current platform-wide RPM
protection, so creating more keys does not bypass the global limit. The API does not publish a universal numeric
Request limits are assigned per organization and shared by all of its API keys, so creating more keys does not raise
the limit. The API does not publish a universal numeric
default or remaining-quota headers.

See [Rate Limiting](/guides/rate-limiting) for details on limits and handling 429 responses.
14 changes: 9 additions & 5 deletions api-reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ SandBase uses standard HTTP status codes, but it does not expose one universal e
Core middleware and several `/v1/*` endpoints return a flat error:

```json
{"error":"API key rate limit exceeded"}
{"error":"organization rate limit exceeded"}
```

Agents platform resources (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v1/mcp-connections*`, `/v1/secrets*`, `/v1/skills*`) return an OpenAI-style object with a stable `code`. See [Agents platform errors](#agents-platform-errors):
Expand Down Expand Up @@ -46,7 +46,7 @@ Use the [OpenAPI specification](/openapi.yaml) for the exact status codes and sc
| `404` | Model or resource is unavailable to the caller | Verify the current model ID or resource ID. |
| `409` | Resource state conflict | Read the latest state before deciding whether a retry is safe. |
| `422` | Semantically invalid resource input | Correct the documented field values. |
| `429` | Per-key or platform-wide request protection | Retry with bounded backoff and jitter when the operation is safe to repeat. |
| `429` | Organization request limit, video concurrency limit, or daily free API allowance | Retry with bounded backoff and jitter when the operation is safe to repeat. |
| `500`, `502`, `503`, `504` | SandBase or upstream failure | Retry only when the operation is safe to repeat. |

Not every endpoint documents every status in this table. The operation's OpenAPI response map is authoritative.
Expand All @@ -59,19 +59,22 @@ Documented authentication messages include:
- `invalid API key`
- `API key has been revoked`
- `API key has expired`
- `insufficient_scope`

A scoped key used outside its scope returns `403` with a nested body instead: `{"error":{"code":"insufficient_scope","message":"API key is not authorized for this endpoint","request_id":"..."}}`. Agents platform resources return the same `code` in their own envelope.

Use `Authorization: Bearer $SANDBASE_API_KEY` for standard endpoints. `POST /v1/messages` also accepts `x-api-key`; protocol-compatible Google endpoints document their own supported key headers.

A revoked key cannot be re-enabled. Create a replacement, update the consuming application, verify it, and then remove any remaining references to the old key.

## Rate limits

Documented flat messages include `API key rate limit exceeded` and `global rate limit exceeded`. SandBase does not publish one universal numeric limit because the effective limit can vary by key and platform capacity.
`organization rate limit exceeded` (`429`) means your organization reached its assigned requests-per-minute limit, which all of its API keys share. Protocol-compatible endpoints return it in their own envelope. SandBase does not publish one universal numeric limit because the limit is assigned per organization.

`daily free API limit exceeded` (`429`) means your organization used its daily allowance for API operations that are currently free. It resets at 00:00 UTC, so do not retry before then.

If a response includes `Retry-After`, respect it. Otherwise use bounded exponential backoff with jitter. See [Rate limits](/guides/rate-limiting) for request-smoothing patterns.

Video submissions have two additional checks. `organization concurrency limit exceeded` (`429`) means your organization already has as many tasks in progress as its account level allows; it clears when a running task finishes. `insufficient credit for estimated video cost` (`402`) means the estimated cost is more than your balance plus credit line; top up before retrying. See [Concurrency limits](/guides/concurrency-limits).
Task submissions have two additional checks. `organization concurrency limit exceeded` (`429`) means your organization already has as many tasks in progress as its account level allows; it clears when a running task finishes. It applies to every model type on `POST /v1/run` and `/v1/api/*`, and to video submissions on other endpoints. `insufficient credit for estimated video cost` (`402`) means the estimated cost is more than your balance plus credit line; top up before retrying. See [Concurrency limits](/guides/concurrency-limits).

## Retry safety

Expand Down Expand Up @@ -112,6 +115,7 @@ Agents, Sessions, Services, Schedules, Skills, MCP connections, and Secrets retu
| 409 | `session_input_pending` | Earlier input is still being delivered | Wait; do not resend with a new key |
| 409 | `session_input_expired` | Input admission expired | Inspect history before sending again |
| 409 | `session_environment_unavailable` | The execution environment is unavailable | Wait or create a new Session |
| 413 | `request_too_large` | The request body, or the Session configuration frozen from it, exceeds the size limit | Send a smaller body; do not retry unchanged |
| 429 | `concurrency_limit` | A Service or Schedule already has a pending Run | Wait for it to finish or cancel it |
| 429 | `quota_exceeded` | Agents quota exceeded | Retry later |
| 502 | `executor_rejected`, `executor_contract_error` | The execution layer rejected the request or answered unexpectedly | Check the Agent configuration, then retry |
Expand Down
Loading
Loading