API overview
Call the capabilities declared for the authenticated, versioned REST API.
The Oxagen REST API serves capabilities declared for its API surface as versioned HTTP endpoints. Use it for server-to-server integrations, your own tooling, and automation pipelines that do not speak MCP.
Base URL: https://api.oxagen.sh
Current version: v1
Every endpoint sits under /v1/, and the version prefix is required. Workspace-scoped endpoints take the org and workspace slugs as the next two path segments:
https://api.oxagen.sh/v1/{orgSlug}/{workspaceSlug}/agent/tools
└┬┘ └───┬───┘ └─────┬─────┘ └────┬────┘
version org workspace capability routeYour first request
List the capabilities the workspace offers as agent tools. The call only reads, and it consumes no credits.
curl -X POST "https://api.oxagen.sh/v1/acme/main/agent/tools" \
-H "Authorization: Bearer $OXAGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "includeExternal": false }'Each entry in tools carries name, description, domain, category, riskLevel (low, medium, or high), requiresApproval, and external.
Design principles
Declared surfaces. A capability contract declares whether it is exposed on API, MCP, agent, or CLI. Shared handlers use the same kernel, but availability differs by contract. Check the declared surfaces before moving an integration.
Thin shell, shared logic. The API server is a thin routing layer over the shared platform library. Business logic lives in the shared layer, not in route handlers, so the API behaves the same way the app does.
Versioned. Every endpoint carries the /v1/ prefix. A breaking change ships in a new version, with a window where both versions run.
Scoped. A workspace key belongs to one org and one workspace. An organization key belongs to one org and can name any workspace in it. You do not pass orgId or workspaceId in request bodies. The path must name the key's own org and workspace, by slug or ID. A path that names another one gets 403 with error.reason key_scope_mismatch.
Request lifecycle
Every route is a thin adapter over invoke(). The gates, the meter, and the audit record are the same ones the MCP server and the in-app agent pass through.
Authentication
Authentication has the full reference. In short:
Authorization: Bearer ox_…Every request needs an Authorization header with a Bearer token. A request without a valid token returns 401 Unauthorized. When IAM denies the capability for the calling principal, the request returns 403 Forbidden with a forbidden error code.
Error format
Errors share one envelope. requestId sits beside error, and the x-request-id response header carries the same value. When a refusal has a more specific cause, error.reason names it, for example key_scope_mismatch or approval_expired:
{
"error": {
"code": "forbidden",
"message": "IAM denied \"list_agent_tools\" for principal: deny"
},
"requestId": "3f1c9a52-7d4e-4b8a-9c1e-2a6f0b5d8e47"
}Common error codes:
| Code | HTTP status | Meaning |
|---|---|---|
unauthorized | 401 | Missing, malformed, invalid, or expired token, a key whose workspace is archived, or a key locked to a purpose this route does not serve |
forbidden | 403 | IAM denied the capability, the capability is not on the API surface, or a role, key scope, or sign-in rule refused the caller. reason names the rule when there is one |
pending_approval | 403 | The rule requires approval. Oxagen recorded an access request (accessRequestId is included) |
decision_rule_denied | 403 | A workspace decision rule refused the call. The message names the rule and its description |
decision_rule_approval_required | 403 | A workspace decision rule wants a person to approve the call first. The API has no way to ask, so the call is refused |
insufficient_credits | 402 | The org has zero credit balance |
billing_suspended | 402 | The org subscription is suspended |
budget_exceeded | 402 | A spend ceiling was reached |
gau_exhausted | 402 | An org with no subscription has used up or outlived its signup grant. reason says which |
not_found | 404 | The resource or capability does not exist, or is outside the token's scope |
conflict | 409 | The request conflicts with the resource's current state. reason names the conflict |
validation_error | 400 | The request body failed schema validation (details lists the issues) |
bad_request | 400 | The capability rejected the input |
internal_error | 500 | Unexpected server error. Include requestId in support tickets |
decision_rules_unavailable | 503 | Oxagen could not read the workspace's decision rules, so it did not admit the call. Retry shortly |
external_rules_unavailable | 503 | The server has not loaded the rules for external tools, so it did not run the call. Retry shortly |
service_overloaded | 503 | The API is at capacity. Retry after the seconds in Retry-After |
A rate-limited request is the one exception to the envelope. It returns 429 with the body { "error": "rate_limited" } and a Retry-After header in seconds.
Capabilities and parity
Capabilities explains the contract model, the naming rule, the route conventions, and how to read the capability registry.
Some flows also require a browser redirect or an approval interaction. Consult the operation's route and contract before assuming it can run unattended.
Rate limits
The API enforces rate limits. Past a limit, the response is 429 Too Many Requests with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Oxagen does not publish per-plan limits. Contact support if you hit sustained rate limiting.
A request body may be up to 4 MiB. A larger body gets 413 with the code bad_request. File uploads to the assistant have their own, larger limit.
Streaming
The chat endpoint (POST /v1/{orgSlug}/{workspaceSlug}/chat/stream) returns a text/event-stream (SSE) response instead of a JSON body. Chat streaming describes the event format.
GET /v1/{orgSlug}/{workspaceSlug}/runs/{runId}/stream streams one run's frames as SSE. Each frame carries an id:. After a dropped connection, resume with the Last-Event-ID header or the after query parameter, and no frame repeats or goes missing.