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
1 change: 0 additions & 1 deletion .vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ export default defineConfig({
'DEPLOYMENT.md',
// Retired Embed, Environment, Endpoint, Deployment, and Site Agent pages live here.
'_archived/**',
'api-reference/webhooks.md',
'agents/endpoint-quickstart.md',
'admin/api-keys.md',
'setup/cli.md',
Expand Down
1 change: 1 addition & 0 deletions .vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const apiReferenceSidebar: DefaultTheme.SidebarItem[] = [
{ text: 'Overview', link: '/api-reference/' },
{ text: 'Authentication', link: '/api-reference/authentication' },
{ text: 'Errors', link: '/api-reference/errors' },
{ text: 'Webhooks', link: '/api-reference/webhooks' },
{ text: 'OpenAPI Spec', link: '/docs/openapi.yaml' },
],
},
Expand Down
5 changes: 3 additions & 2 deletions api-reference/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ Gemini GenerateContent and Interactions operations accept `x-goog-api-key`, `Aut
### Extraction Logic

Standard endpoints read `Authorization: Bearer <key>` only. The Anthropic Messages middleware reads `x-api-key`
first and then falls back to the Bearer header. Agents platform resources are the other exception: they accept
`Authorization: Bearer` or `X-API-Key` (see below). A missing supported credential returns `401 Unauthorized`.
first and then falls back to the Bearer header. Agents platform resources accept `Authorization: Bearer` or
`X-API-Key` (see below). The [webhook Events API](/api-reference/webhooks) (`/events/webhooks*`) also accepts either
header and uses the Bearer header when both are present. A missing supported credential returns `401 Unauthorized`.

### Agents platform resources

Expand Down
2 changes: 2 additions & 0 deletions api-reference/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Agents platform resources (`/v1/agents*`, `/v1/services*`, `/v1/schedules*`, `/v
{"error":{"type":"invalid_request_error","code":"invalid_request","param":null,"message":"invalid request"}}
```

The webhook Events API (`/events/webhooks*`) returns `{"code":<http status>,"message":"..."}`. See [Webhooks](/api-reference/webhooks#authentication-and-errors).

OpenAI-compatible endpoints retain an OpenAI-compatible envelope. `POST /v1/messages` retains an Anthropic-compatible envelope:

```json
Expand Down
17 changes: 17 additions & 0 deletions api-reference/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@ curl -X PATCH https://api.sandbase.ai/events/webhooks/wh_01... \

`signatureSecret` cannot be viewed or changed through `PATCH`; a request containing it is rejected. If the secret is lost, delete and recreate the webhook. You can also view, edit, enable or disable, and delete registrations from the [Console Webhooks page](https://www.sandbase.ai/console/webhooks).

`GET` returns a JSON array of webhooks (without `signatureSecret`), `PATCH` returns the updated webhook, and `DELETE`
returns `204 No Content`.

### Authentication and errors

The Events API accepts the API key as `Authorization: Bearer $SANDBASE_API_KEY` or `X-API-Key: $SANDBASE_API_KEY`. When
both are sent, the Bearer header is used.

Errors use their own body, with the HTTP status repeated in `code`:

```json
{"code": 400, "message": "resourceType must be \"model\""}
```

Branch on the HTTP status. `401` means the key is missing, invalid, or revoked; `400` means the request body or
subscription is invalid; `404` (`webhook not found`) means the webhook does not exist in your organization.

## Per-task model callback

For one asynchronous image, video, or audio task, add `webhook_url` to `POST /v1/run` instead of registering a new webhook:
Expand Down
118 changes: 118 additions & 0 deletions public/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ tags:
description: Model catalog
- name: Tasks
description: Asynchronous model task status, settlement, and usage
- name: Webhooks
description: Register organization webhooks for asynchronous model generation events
- name: Account
description: Organization balance and API execution history
- name: Skills
Expand Down Expand Up @@ -640,6 +642,72 @@ paths:
'403': { description: API Key spending limit reached or organization disabled, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } }
'404': { description: 'Task is unknown, deleted, not native, or not owned by the current API key', content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } }
'500': { description: Task lookup failed, content: { application/json: { schema: { $ref: '#/components/schemas/VolcengineError' } } } }
# --- Webhooks ---------------------------------------------------
/events/webhooks:
get:
operationId: listWebhooks
tags: [Webhooks]
summary: List webhooks
description: Lists the organization's webhooks. signatureSecret is never returned.
security:
- BearerAuth: []
- WebhookApiKey: []
responses:
'200': { description: Webhooks, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Webhook' } } } } }
'401': { description: 'Missing, invalid, or revoked API key', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'500': { description: Webhook lookup failed, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
post:
operationId: createWebhook
tags: [Webhooks]
summary: Create a webhook
description: Registers a webhook for asynchronous model generation events. The response is the only time signatureSecret is returned.
security:
- BearerAuth: []
- WebhookApiKey: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/WebhookCreateRequest' }
responses:
'200': { description: Webhook created, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookCreated' } } } }
'400': { description: 'Invalid body, URL, resourceType, or events', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'401': { description: 'Missing, invalid, or revoked API key', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'500': { description: Webhook creation failed, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
/events/webhooks/{webhook_id}:
parameters:
- { name: webhook_id, in: path, required: true, schema: { type: string }, description: Webhook ID returned by create or list. }
patch:
operationId: updateWebhook
tags: [Webhooks]
summary: Update a webhook
description: Updates the fields sent; omitted fields remain unchanged. The resulting resourceType and events subscription must be valid. signatureSecret cannot be changed.
security:
- BearerAuth: []
- WebhookApiKey: []
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/WebhookUpdateRequest' }
responses:
'200': { description: Updated webhook, content: { application/json: { schema: { $ref: '#/components/schemas/Webhook' } } } }
'400': { description: 'Invalid body or subscription, or the request includes signatureSecret', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'401': { description: 'Missing, invalid, or revoked API key', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'404': { description: 'webhook not found', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'500': { description: Webhook update failed, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
delete:
operationId: deleteWebhook
tags: [Webhooks]
summary: Delete a webhook
security:
- BearerAuth: []
- WebhookApiKey: []
responses:
'204': { description: Webhook deleted }
'401': { description: 'Missing, invalid, or revoked API key', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'404': { description: 'webhook not found', content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
'500': { description: Webhook deletion failed, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookError' } } } }
# --- Account ----------------------------------------------------
/v1/account/balance:
get:
Expand Down Expand Up @@ -3872,6 +3940,11 @@ components:
in: query
name: key
description: Google SDK compatibility fallback. Prefer x-goog-api-key or Authorization because URLs can be logged.
WebhookApiKey:
type: apiKey
in: header
name: X-API-Key
description: Accepted by the webhook Events API (/events/webhooks*). Authorization Bearer takes precedence when both are sent.
AgentsApiKey:
type: apiKey
in: header
Expand Down Expand Up @@ -4245,6 +4318,51 @@ components:
description: Provider-returned download URL. It can be empty until the provider makes a URL available.
created_at: { type: string, format: date-time }

Webhook:
type: object
required: [id, name, url, resourceType, events, enabled, createdAt]
properties:
id: { type: string }
name: { type: string }
url: { type: string, format: uri }
resourceType: { type: string, description: 'model for new registrations. Retired types (session, sandbox, template) can still appear on old registrations.' }
events: { type: array, items: { type: string }, description: Empty means all events in the resource type. }
enabled: { type: boolean }
createdAt: { type: string, format: date-time }
WebhookCreated:
allOf:
- $ref: '#/components/schemas/Webhook'
- type: object
required: [signatureSecret]
properties:
signatureSecret: { type: string, description: HMAC signing secret. Returned only in this response. }
WebhookCreateRequest:
type: object
required: [name, url, resourceType]
properties:
name: { type: string }
url: { type: string, format: uri, description: HTTP(S) endpoint. Use HTTPS in production. }
resourceType: { type: string, enum: [model] }
events:
type: array
items: { type: string, enum: [model.generation.completed, model.generation.failed, model.generation.timeout, model.generation.cancelled] }
description: Empty or omitted means all current and future events in the resource type.
enabled: { type: boolean, default: true }
signatureSecret: { type: string, description: Optional. Omit to let SandBase generate a high-entropy secret. }
WebhookUpdateRequest:
type: object
properties:
name: { type: string }
url: { type: string, format: uri }
resourceType: { type: string, enum: [model] }
events: { type: array, items: { type: string, enum: [model.generation.completed, model.generation.failed, model.generation.timeout, model.generation.cancelled] } }
enabled: { type: boolean }
WebhookError:
type: object
required: [code, message]
properties:
code: { type: integer, description: The HTTP status code. }
message: { type: string }
TaskCostResponse:
type: object
required: [id, status, settled, currency, cost, estimated_cost, usage]
Expand Down
1 change: 0 additions & 1 deletion scripts/validate-built-public-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ const forbidden = [
[/\/v1\/generations(?:\/|\b)/i, 'withdrawn generation API'],
[/\/v1\/blog\/assets(?:\/|\b)/i, 'internal Blog publishing storage API'],
[sandboxAPIPath, 'Sandbox API'],
[/\/events\/webhooks(?:\/|\b)/i, 'Sandbox event webhook API'],
[/\/v1\/endpoints\/[A-Za-z0-9_{}%.-]+\/mcp\b/i, 'Endpoint MCP transport'],
[/\/v1\/endpoint_runtime_profiles\b/i, 'Endpoint runtime-profile discovery'],
[/\/v1\/mcp(?!-connections)(?:\/|\b)/i, 'generic MCP transport or discovery'],
Expand Down
1 change: 0 additions & 1 deletion scripts/validate-doc-content.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const roots = ['admin', 'agents', 'sandbox', 'api-reference', 'for-agents', 'get
const excluded = [
'agents/deployments.md',
'agents/endpoint-quickstart.md',
'api-reference/webhooks.md',
'setup/cli.md',
'setup/groups.md',
]
Expand Down
23 changes: 15 additions & 8 deletions scripts/validate-public-api-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ for (const [label, content] of [['Capabilities', capabilitiesPage], ['Vision', v
assert.match(legacySeedanceReference, /^canonical:\s*\/docs\/model-api-reference\/official-native-api\/$/m, 'Legacy Seedance reference must canonicalize to Official Native API')
assert.match(legacySeedanceReference, /^robots:\s*noindex,follow$/m, 'Legacy Seedance reference must not compete in search results')
assert.doesNotMatch(readme, /deepseek\/deepseek-v3\b/i, 'README must not recommend the retired DeepSeek V3 model')
assert.doesNotMatch(readme, /API Reference[^\n]*webhooks/i, 'README must not advertise the hidden Webhooks page')
assert.doesNotMatch(readme, /\b\d[\d,]*\+?\s+models and APIs\b/i, 'README must not hard-code time-sensitive catalog counts')
for (const overview of catalogOverviews) {
assert.doesNotMatch(overview, /currently publishes API reference pages for \d/i, 'Catalog overviews must not hard-code time-sensitive counts')
Expand Down Expand Up @@ -176,6 +175,11 @@ for (const [publicPath, pathItem] of Object.entries(openapiDocument.paths)) {
operation.responses['401']?.content?.['application/json']?.schema,
`${method.toUpperCase()} ${publicPath} must document its implemented JSON authentication-error envelope`,
)
if (publicPath.startsWith('/events/webhooks')) {
// Webhook management authenticates the key only; it never applies spending limits.
assert.ok(!operation.responses['402'], `${method.toUpperCase()} ${publicPath} must not claim spending-limit rejection`)
continue
}
if (publicPath === '/v1/tasks/{task_id}/cost') {
assert.ok(!operation.responses['402'], 'Task cost lookup must preserve its implemented spending-limit exception')
assert.ok(operation.responses['403'], 'Task cost lookup must document scoped-key rejection')
Expand All @@ -195,14 +199,14 @@ assert.match(config, /'_archived\/\*\*'/, 'Archived API pages must stay excluded
for (const archivedSource of ['guides/site-agent-integration.md', 'use-cases', 'api-reference/embeds', 'api-reference/environments', 'api-reference/endpoints/mcp.md', 'api-reference/endpoints/acp.md']) {
assert.ok(!existsSync(path.join(root, archivedSource)), `${archivedSource} is retired and must live under _archived/`)
}
assert.match(config, /'api-reference\/webhooks\.md'/, 'Sandbox event webhook reference must stay excluded from the public build')
assert.doesNotMatch(config, /'api-reference\/webhooks\.md'/, 'The model webhook reference must be published')
assert.match(config, /'agents\/endpoint-quickstart\.md'/, 'Retired Service quickstart must stay excluded from the public build')
assert.match(config, /'admin\/api-keys\.md'/, 'Duplicate API Keys guide must stay excluded from the public build')
assert.match(config, /'setup\/cli\.md'/, 'Duplicate CLI setup page must stay excluded from the public build')
assert.match(config, /'setup\/groups\.md'/, 'Retired Platform Groups page must stay excluded from the public build')
assert.doesNotMatch(platformSidebar, /Service quickstart/, 'Retired Service quickstart must not appear in the docs sidebar')
assert.doesNotMatch(sidebar, /\/api-reference\/sandboxes?\b/i, 'Sidebar must not link to sandbox API pages')
assert.doesNotMatch(sidebar, /\/api-reference\/webhooks\b/i, 'Sidebar must not link to Sandbox event webhook APIs')
assert.match(sidebar, /\/api-reference\/webhooks\b/, 'Sidebar must link to the model webhook reference')
assert.equal((modelSidebar.match(/text: 'Inference APIs'/g) ?? []).length, 0, 'Model API navigation must keep normalized Inference APIs in the concept/API reference docs')
assert.doesNotMatch(sidebar, /text: 'Embed Configs'/, 'Platform API sidebar must not expose the unmaintained Embed Config module')
assert.doesNotMatch(platformSidebar, /text: 'Models'/, 'Platform API sidebar must direct model discovery to the standalone Model API Reference')
Expand Down Expand Up @@ -236,7 +240,13 @@ assert.doesNotMatch(openapi, /^ \/v1\/skills\/\{[^}]+\}\/mcp-publications:$/m,
assert.doesNotMatch(openapi, /^ \/v1\/skill-mcp-publications(?:\/\{[^}]+\})?:$/m, 'Skill MCP publication management must not be public')
assert.doesNotMatch(openapi, /^ \/v1\/capabilities\/\{[^}]+\}\/mcp:$/m, 'Capability MCP transport must not be public')
assert.doesNotMatch(openapi, /^ \/v1\/skill-mcp\/\{[^}]+\}\/mcp:$/m, 'Published Skill MCP transport must not be public')
assert.doesNotMatch(openapi, /^ \/events\/webhooks(?:\/\{[^}]+\})?:$/m, 'Sandbox event webhook paths must not be public')
for (const webhookPath of ['/events/webhooks', '/events/webhooks/{webhook_id}']) {
for (const operation of Object.values(openapiDocument.paths[webhookPath] ?? {}).filter((value) => value?.operationId)) {
assert.deepEqual(operation.security, [{ BearerAuth: [] }, { WebhookApiKey: [] }], `${webhookPath} must document Bearer and X-API-Key authentication`)
}
}
assert.ok(openapiDocument.paths['/events/webhooks']?.get && openapiDocument.paths['/events/webhooks']?.post, 'Webhook list and create must be public')
assert.ok(openapiDocument.paths['/events/webhooks/{webhook_id}']?.patch && openapiDocument.paths['/events/webhooks/{webhook_id}']?.delete, 'Webhook update and delete must be public')
const geminiGeneratePath = openapiDocument.paths['/v1beta/models/{model}:generateContent']?.post
const geminiStreamPath = openapiDocument.paths['/v1beta/models/{model}:streamGenerateContent']?.post
assert.ok(geminiGeneratePath, 'Public OpenAPI must expose the implemented Gemini GenerateContent endpoint')
Expand Down Expand Up @@ -667,9 +677,7 @@ assert.match(uploadMediaResponse, /required: \[url, filename, size, type, conten
assert.match(modelApiOverview, /\/api-reference\/models\/(?:assets|upload)/, 'Model API overview must link to the media asset references')
assert.doesNotMatch(openapi, /^ \/default\/v1(?:\/|:)/m, 'Internal Console paths must not be public')

const unpublishedFiles = new Set([
'api-reference/webhooks.md',
])
const unpublishedFiles = new Set([])

function inspectPublishedSources(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
Expand All @@ -687,7 +695,6 @@ function inspectPublishedSources(directory) {
assert.doesNotMatch(content, /\/default\/v1(?:\/|\b)/, `${relative} must not expose internal Console API paths`)
assert.doesNotMatch(content, /\/v1\/generations(?:\/|\b)/, `${relative} must not expose the withdrawn generation path`)
assert.doesNotMatch(content, /\/v1\/blog\/assets(?:\/|\b)/, `${relative} must not expose the internal Blog publishing storage API`)
assert.doesNotMatch(content, /\/events\/webhooks(?:\/|\b)/, `${relative} must not expose Sandbox event webhook paths`)
assert.doesNotMatch(content, /\brun_abc123\b/, `${relative} must not assume a run ID prefix`)
assert.doesNotMatch(content, /\btask_abc123\b/, `${relative} must not assume a task ID prefix`)
assert.doesNotMatch(content, /\bcred_01/, `${relative} must use the implemented sec_ Managed Credential ID prefix`)
Expand Down
1 change: 0 additions & 1 deletion scripts/validate-seo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import YAML from 'yaml'
const roots = ['admin', 'agents', 'sandbox', 'api-reference', 'for-agents', 'getting-started', 'guides', 'models', 'setup', 'store', 'model-api-reference']
const excluded = [
'agents/endpoint-quickstart.md',
'api-reference/webhooks.md',
'setup/cli.md',
'setup/groups.md',
]
Expand Down
Loading