Management endpoints live under /api/v1/; invocation, MCP, metrics,
and OAuth discovery/authorization also expose the public paths called out
below. Management auth uses either:
- API key:
X-Orva-API-Key: orva_xxx...header. Used by curl, CI, external callers. - Session cookie: set by
POST /api/v1/auth/login. Used by the dashboard.
API keys carry a permission set. The bootstrap admin key has all four:
invoke, read, write, admin. Operator-issued keys can be
narrowed.
Error envelope (every 4xx/5xx):
{
"error": {
"code": "POOL_AT_CAPACITY",
"message": "function pool at capacity for 019df200-7b00-7e00-9c00-aab1cd2e3f40",
"request_id": "req_abc",
"hint": "inspect pool limiting_reason; raise max_warm only for operator_max",
"retry_after_s": 5,
"details": {"function_id": "019df200-7b00-7e00-9c00-aab1cd2e3f40", "current": 16, "limit": 16}
}
}Retry-After HTTP header set in parallel when retry_after_s is
present. Full code catalog in ERRORS.md.
First-run only. Creates the admin user. Returns 409 if a user already exists.
// request
{"username": "admin", "password": "AdminPass123!Secure"}
// response 201
{"user": {"id": "u_xxx", "username": "admin"}, "expires_at": "..."}Sets the session cookie.
{"username": "admin", "password": "..."}Returns the current user (cookie-authed).
Returns {"has_user": bool} so the UI knows whether to route to
/onboarding or /login.
Rotates the cookie's expiry forward by 7 days.
Invalidates the session.
POST /api/v1/auth/change-password— change the current user's password.GET /api/v1/auth/sessions— list active login sessions.DELETE /api/v1/auth/sessions/{prefix}— revoke a session by token prefix.GET /api/v1/oauth/connected-apps— list authorized OAuth clients.DELETE /api/v1/oauth/connected-apps/{id}— revoke a connected client.
Create a function record.
{
"name": "my-fn",
"runtime": "node", // node | python
"entrypoint": "handler.js", // optional, defaults match the runtime
"memory_mb": 128,
"cpus": 1,
"timeout_ms": 30000,
"env_vars": {"NODE_ENV": "production"},
"network_mode": "none" // none (default) | egress
}network_mode controls per-function network access:
none(default) — isolated net namespace, loopback only. DNS / TCP / UDP all blocked. Best for pure-compute handlers.egress— userspace TCP/UDP stack via nsjail--user_net. Function can call external HTTPS APIs. Host interfaces stay isolated. The instance-wide egress policy is compiled into every such sandbox; a blocked destination gives the handlerECONNREFUSED.
Toggling on an existing function via PUT /api/v1/functions/{id}
drains the warm pool so the next invocation picks up the new mode.
List all functions. Optional ?status=active|inactive, ?runtime=....
Single function record.
Partial update. Whitelisted fields: name, description, entrypoint,
timeout_ms, memory_mb, cpus, env_vars, network_mode,
max_concurrency, concurrency_policy, auth_mode, rate_limit_per_min,
status.
status accepts only active | inactive. Setting inactive causes
POST /fn/<id> to return 409 NOT_ACTIVE.
auth_mode accepts public | platform_key | signed and governs how
POST /fn/<id> is authorized. concurrency_policy accepts reject | queue
and decides what happens once max_concurrency is reached (reject returns
429 FUNCTION_BUSY). rate_limit_per_min is a per-client-IP cap; exceeding it
returns 429 RATE_LIMITED with Retry-After: 60.
Removes the row + the on-disk versions dir. Irreversible.
Deploy from JSON.
{
"code": "module.exports = async () => ({ok:true});",
"filename": "handler.js",
"dependencies": "lodash@^4.17.21" // optional, becomes package.json or requirements.txt
}Returns 202 with the deployment record. Build runs asynchronously.
Deploy from a tarball (multipart upload).
Roll back to a prior version.
{"deployment_id": "019df210-1234-7000-8000-deadbeef0001"} // or {"code_hash": "abc..."}Returns 200 with a synthetic deployment row of source: "rollback".
Returns 410 VERSION_GCD if the target version was pruned by the GC.
Returns the function's current code + dependencies as JSON. Used by the Editor view.
Compares the handler source + dependency manifest between two past
succeeded deployments. Both from and to must be deployment UUIDs
belonging to this function.
format=json(default — dashboardCompare versionsview) returns{from, to, files: [{path, kind:"handler"|"manifest", before, after, added, removed}]}.before/aftercarry the raw file bytes so the browser-side merge viewer can compute its own hunks.format=unifiedreturnstext/x-diffwith git-style hunks per file (--- a/path/+++ b/path/@@ …). Consumed byorva diff.
Errors:
- 400
VALIDATIONiffromandtoare equal, belong to different functions, or aren't in statussucceeded. - 404
VERSION_NOT_FOUNDif either deployment ID is unknown; details include the requested ID. - 410
VERSION_GCDif either version's source tree was pruned by the GC.details.available_hasheslists the surviving on-disk versions so the caller can retry against a still-archived target.
Deployment history for a function. Optional ?limit=N (default 50).
Calls the function. id is the function's UUID (the same value returned in the id field by GET /api/v1/functions).
(e.g. function 019df200-7b00-7e00-9c00-aab1cd2e3f40 → URL
/fn/019df200-7b00-7e00-9c00-aab1cd2e3f40). Method,
headers, body, query, and path (everything after /{id}) are all
passed to the handler as event.
Response is whatever the handler returns. HTTP status is 200 unless
the handler throws or returns an AWS-shape {statusCode, body}.
Custom routes (e.g. /webhooks/stripe) reach the same handler — see
the routes section below.
Single deployment record.
Build logs for that deployment.
Server-sent events stream of build progress. Live tail; closes when
the build reaches a terminal state (succeeded | failed).
List recent invocations. Optional ?function_id=..., ?limit=N.
Single execution row (status, duration, cold_start flag).
The function's stderr from this invocation.
GET /api/v1/executions/{id}/request— return the captured invocation request.DELETE /api/v1/executions/{id}— delete one execution record.POST /api/v1/executions/bulk-delete— delete matching execution records.POST /api/v1/executions/{id}/replay— replay a captured request.GET /api/v1/tracesandGET /api/v1/traces/{id}— list trace-wide summaries with opaque stable cursors and inspect the complete causal waterfall.GET /api/v1/functions/{id}/baseline— return the function's trace baseline.GET /api/v1/activity— list the operator activity feed.
List secret keys for a function. Values are not returned (encrypted at rest; only injected into the sandbox at spawn time).
Upsert. Body: {"key": "STRIPE_KEY", "value": "sk_..."}. Triggers a
pool refresh so the next invocation sees the new value.
Remove. Triggers a pool refresh.
Operator-facing function resources use the normal API-key/session auth:
GET /api/v1/functions/{id}/kvGET|PUT|DELETE /api/v1/functions/{id}/kv/{key}POST /api/v1/functions/{id}/kv/{key}/incrPOST /api/v1/functions/{id}/kv/{key}/casGET|POST /api/v1/functions/{id}/fixturesGET|PUT|DELETE /api/v1/functions/{id}/fixtures/{name}GET|POST /api/v1/functions/{id}/cronPUT|DELETE /api/v1/functions/{id}/cron/{schedule_id}GET /api/v1/cron— list schedules across all functions.
KV keys must be non-empty UTF-8 up to 256 characters and values must be valid
JSON up to 64 KiB. Internal SDK batches accept at most 100 operations and are
atomic. For put/increment/CAS, omitted ttl_seconds preserves an existing
expiry (new keys remain persistent), zero clears expiry, a positive value sets
or refreshes it, and a negative value returns 400 VALIDATION.
The /_kv and /_internal route families are sandbox-SDK transport
endpoints authenticated with a process-signed, function-scoped credential.
The verified claim supplies caller identity; caller headers are ignored. A
credential expires when orvad restarts, KV access is restricted to its own
namespace, cron upsert is restricted to its own schedules, and user spans must
name an active execution owned by the credential's function. Invokes and job
enqueue may target another function while retaining signed caller attribution.
These routes are not an operator API and do not accept long-lived API keys.
POST /api/v1/jobs— enqueue a background job.GET /api/v1/jobsandGET /api/v1/jobs/{id}— list or inspect jobs.POST /api/v1/jobs/{id}/retry— retry a failed job.DELETE /api/v1/jobs/{id}— delete a job.
Inbound function endpoints:
GET|POST /api/v1/functions/{id}/inbound-webhooksGET|PUT|DELETE /api/v1/functions/{id}/inbound-webhooks/{webhook_id}
Outbound event delivery:
GET|POST /api/v1/webhooksGET|PUT|DELETE /api/v1/webhooks/{id}POST /api/v1/webhooks/{id}/testGET /api/v1/webhooks/{id}/deliveriesPOST /api/v1/webhooks/deliveries/{id}/retry
Map a custom URL to a function so external callers don't need the function ID.
List custom routes.
{"path": "/webhooks/stripe", "function_id": "019df200-7b00-7e00-9c00-aab1cd2e3f40", "methods": "POST"}methods accepts * for all methods or comma-separated (GET,POST).
Reserved prefixes (/api/, /auth/, /web/, /_orva/) are rejected.
Remove a route.
Per-function autoscaler tuning.
Read the row.
{
"function_id": "019df200-7b00-7e00-9c00-aab1cd2e3f40",
"min_warm": 2,
"max_warm": 32,
"idle_ttl_seconds": 600,
"scale_to_zero": false
}Fields are partial — unspecified ones keep the prior value (or default
for new rows). Defaults are min 1, max 50, idle TTL 600 seconds, and
scale-to-zero off. Pool Controller v2 derives desired capacity from demand;
the removed target_concurrency field returns 400 VALIDATION.
List keys. Returns prefixes, names, last_used_at, expires_at. Never returns the plaintext key.
{
"name": "ci-deployer",
"permissions": ["invoke", "read", "write"], // optional, defaults to all 4
"expires_in_days": 90 // or expires_at: "ISO timestamp"
}Returns the plaintext key once. Save it immediately — it's not recoverable.
Revoke a key.
A channel bundles N deployed functions under a name and a static bearer
token. Presenting that token at /mcp exposes ONE MCP tool per
bundled function (invoke-only) and nothing else — no Orva-management
surface. Token format: orva_chn_<32 hex>. Channel tokens are
explicitly rejected at every /api/v1/* endpoint (401); they're
MCP-only.
Auth header at /mcp — channel tokens accept either form, same as
operator API keys:
Authorization: Bearer orva_chn_<token> # spec-standard, recommended
X-Orva-API-Key: orva_chn_<token> # parity with the REST API
The REST endpoints below (CRUD on /api/v1/channels) are operator-
managed and require an API key or session cookie — channel tokens
themselves cannot manage channels.
List channels. Returns {channels: [...]} with name, description,
prefix, function_count, last_used_at, expires_at, created_at.
{
"name": "support-bot",
"description": "Support workflow toolkit", // optional
"function_ids": ["<uuid>", "<uuid>"],
"expires_in_days": 30 // optional; or expires_at: "ISO timestamp"
}Returns the plaintext token once in the token field. Save it
immediately — it's not recoverable. Two functions whose names
snake_case to the same MCP tool name are rejected with 400 / TOOL_NAME_COLLISION.
Detail with the bundled function set + per-function description overrides.
Update name / description / expires_at. Function set is unchanged.
{
"function_ids": ["<uuid>", ...],
"descriptions": {"<uuid>": "tool description override"} // optional
}Replaces the function set wholesale. Junction descriptions on overlapping function IDs are preserved unless explicitly overridden.
Re-issues the bearer token. Returns {token: "orva_chn_..."} once;
the previous token stops working immediately.
Cascade — removes the channel and every junction row.
{"status": "ok"} when orvad is up. Used by Docker HEALTHCHECK and
load balancers.
Prometheus text format.
Same data, JSON shape, used by the dashboard.
Prometheus also scrapes the unauthenticated GET /metrics path.
GET /api/v1/backupandPOST /api/v1/restore— download or restore an instance backup.GET /api/v1/system/storage— inspect disk/database usage.POST /api/v1/system/vacuum— compact the SQLite database.GET|POST /api/v1/firewall/rulesPUT|DELETE /api/v1/firewall/rules/{rule_id}POST /api/v1/firewall/resolveGET|PUT /api/v1/firewall/dns
GET /api/v1/firewall/rules returns {"rules": [...], "status": {...}}, and
POST /api/v1/firewall/resolve returns the same status object. It describes
the compiled sandbox egress policy (see
SECURITY.md):
{
"ipv4": ["169.254.0.0/16", "93.184.216.34/32"],
"ipv6": ["fd00:ec2::254/128"],
"hostname_map": {"example.com": ["93.184.216.34"]},
"backend": "nstun",
"enforced": true,
"policy_generation": "3f9a1c0d4b7e2851",
"policy_rule_counts": {"v4": 8, "v6": 2, "allow": 7, "reject": 3},
"policy_stale": false,
"last_success_at": "2026-08-09T11:02:14Z",
"control_plane_allow": {"addrs": ["172.17.0.1"], "port": 8443},
"unenforced_rules": [
{"id": 12, "value": "*.corp.com",
"reason": "wildcard hostnames are not enforceable: egress policy matches IP/CIDR, not DNS names. Use a CIDR or an exact hostname."}
]
}| Field | Meaning |
|---|---|
ipv4 / ipv6 |
The REJECT prefixes actually present in the compiled policy — derived from what is enforced, not from the raw table |
hostname_map |
hostname rule → the addresses currently resolved for it |
backend |
Always "nstun". Enforcement is nsjail NSTUN rules loaded per sandbox |
enforced |
A policy has compiled and is in use. false means egress functions refuse to spawn, since NSTUN's no-match default is ALLOW |
policy_generation |
16 hex chars — a hash of the exact bytes handed to nsjail. Changes only when enforcement changes |
policy_rule_counts |
Compiled rule counts: v4, v6, allow (carve-outs), reject (blocklist) |
policy_stale |
A recompile failed and the last known-good generation is still in force. Read with last_compile_error |
last_compile_error |
Present only on failure |
last_success_at |
RFC 3339 timestamp of the last successful compile |
control_plane_allow |
The narrow ALLOW that keeps orvad's internal SDK (orva.kv / orva.jobs / orva.invoke) reachable: exact addresses, exact port, TCP only |
unenforced_rules |
Stored rules deliberately not compiled, each with a reason. Wildcard rules land here. Surfaced so the UI never implies a rule is in force when it isn't |
nftables_available was removed, with no compatibility alias. The field
described a host-firewall mechanism that no longer exists, and reporting it as
permanently true would be a lie in the API. Clients that read it should read
enforced (is a policy in force) and backend instead.
Creating or enabling a wildcard rule now fails with 400 VALIDATION;
wildcards cannot be expressed as packet rules. Existing wildcard rows are left
untouched and reported in unenforced_rules.
Server-sent events stream of:
event: metrics— periodic 5-second snapshotsevent: execution— every new invocationevent: deployment— every status / phase change
Browser EventSource automatically reconnects. Cookie auth (API-key auth not supported on EventSource — browsers can't set custom headers).
List supported runtimes.
The seccomp policy catalog. Useful for the dashboard's "what is this function allowed to do" tooltip.
The in-product agentic chat (the dashboard's AI section). Requires
a configured provider (BYO key). Streaming endpoints emit
text/event-stream; everything else is JSON. All paths require admin.
Send a user message and stream the assistant turn. Body carries
conversation_id (or omit to start one), content, and the selected
provider/model/thinking level. Response is SSE: message_start,
delta (text), thinking, tool_call, tool_result,
awaiting_approval, message_end, error. Long pre-token gaps are
kept alive with : ping comment frames.
List conversations (most-recently-updated first).
Create an empty conversation.
Delete every conversation and cascade-delete their messages and tool calls in
one operation. Returns {"deleted": N}. Responds with 409 CONVERSATION_BUSY without deleting anything when any conversation has a turn
in progress.
Fetch one conversation with its full message + tool-call timeline.
Rename ({"title": "..."}) or archive ({"archived": true}).
Delete a conversation and all its messages + tool calls (cascade).
List messages, optionally ?since_seq=N for incremental loads.
Truncate the last assistant turn and re-run it. SSE, same frames as
/chat.
Replace a user message's content, truncate everything after it, and re-run the turn. SSE. (There is no branching history — the tail is discarded.)
Delete a message and every message + tool call after it (truncate by
seq).
Resolve a tool call that is awaiting_approval and resume the stream
(approve) or skip it (reject). SSE.
List, upsert, and remove provider configs (provider, label, base URL, API key). Keys are encrypted at rest with the same cipher as function secrets and never returned in responses.
List the models the configured provider/endpoint reports.
Read/update assistant settings: default provider/model, thinking level,
and approval policy (all_writes / destructive_only / auto). The
max_tool_iterations response field is retained for API compatibility but is
an internal runaway-work guard fixed at 25; values supplied by PUT are
ignored and normalized to 25.
Persist the dashboard's active provider/model/thinking selection.
POST /mcp is the Streamable HTTP MCP endpoint. Operator API keys expose
the management tools; channel tokens expose only the functions bundled into
that channel.
The transport is stateless and speaks MCP 2026-07-28, negotiating down
for older clients (server/discover reports every supported version). Three
things follow that are visible on the wire:
- No
initializehandshake is required. A 2026-07-28 request carriesprotocolVersionandclientCapabilitiesinparams._meta(clientInfois optional), plusMcp-Protocol-VersionandMcp-Methodheaders — and, fortools/call/resources/read/prompts/get, anMcp-Nameheader equal to the name in the body. The headers let a proxy route on the operation without parsing the body, so one that disagrees with the body is rejected (-32020), not ignored. Declaring anyprotocolVersionin_metacommits the request to that validated path; the legacy handshake is reached by omitting it, and still returns200. - No
Mcp-Session-Idis issued, and none is read. Any request may be served by any instance. GET /mcpandDELETE /mcpreturn405. There is no session to resume or terminate, and no long-lived server→client stream to attach to.
List results carry the new ttlMs and cacheScope hints. Orva returns
cacheScope: "private" — the tool catalog is scoped to the caller's permissions
and to their channel, so it is never safe for an intermediary to share — and
ttlMs: 0, because the catalog changes on any deploy, channel edit, or
permission change.
OAuth-capable MCP clients discover and authorize through:
GET /.well-known/oauth-protected-resource[/mcp]GET /.well-known/oauth-authorization-server[/mcp]GET /.well-known/openid-configuration[/mcp]POST /registerGET|POST /oauth/authorizePOST /oauth/tokenPOST /oauth/revoke