Apps
Traffic — all apps, last 7 days
By task (7 days)
| Task | Requests | OK | Errors | Prompt tok | Output tok | Avg ms |
|---|
Recent requests
| Time (UTC) | Task | Status | Stream | In ch | Out ch | Tokens | ms | Key |
|---|
| Task | Model | Max out | Rate/min | Stream | JSON | Enabled |
|---|
A client can only name a task — the model, caps, and capabilities above are decided here, server-side. Nothing outside this table is reachable through the proxy.
Provider key slots
| Provider | Key | Status | Added |
|---|
Keys are AES-256-GCM encrypted at rest and never shown again after saving — only the last 4 characters. Traffic is attributed to the key slot that served it.
Authentication mode
App bearer tokens
| Prefix | Label | Created | Status |
|---|
The full token is shown once at creation. Keep at least one live token; create a second to rotate without downtime. In Public mode tokens are optional (used only for attribution).
API Reference
Standardized gateway API — base URL
Surfaces at a glance
| Surface | Prefix | Auth | Consumers |
|---|---|---|---|
| App API | /v1 |
X-App-Id always; Authorization: Bearer app token only when the app's auth mode is token (portal-switchable) |
Your apps — mobile clients (public mode) and server-to-server callers (token mode) |
| Admin API | /admin/v1 |
Authorization: Bearer <ADMIN_TOKEN> |
This portal (a thin client over exactly these endpoints) — and you, via curl |
All requests/responses are JSON except SSE streams; timestamps are ISO-8601 UTC.
The canonical spec lives in the repo: gateway/API.md + gateway/openapi.yaml.
POST /v1/invoke — the single proxy endpoint
The client names a task; the server decides model, caps, and safety. Nothing outside the app's task allowlist is reachable — a leaked credential cannot generate images, pick models, or lift output caps: the capability does not exist on this surface.
Headers
# auth_mode = 'public' (mobile apps — nothing provisioned at build time):
X-App-Id: <app-id>
Content-Type: application/json
# auth_mode = 'token' (server-to-server callers):
X-App-Id: <app-id>
Authorization: Bearer amp_<app-id>_… # created in this portal, revocable
Content-Type: application/json
Request body
{
"task": "chat", // REQUIRED — must be allowlisted for the app
"message": "…", // REQUIRED — clamped to task.max_input_chars
"systemInstruction": "…", // optional — only if task.max_system_chars > 0
"history": [ // optional — last task.max_history_turns kept
{ "role": "user", "text": "…" },
{ "role": "agent", "text": "…" } // "agent" and "model" both accepted
],
"generationConfig": { // optional — every field clamped server-side
"temperature": 0.3, // clamped to [0, task.temperature_max]
"maxOutputTokens": 1024, // clamped to task.max_output_tokens
"topP": 0.95,
"responseMimeType": "application/json" // honored only if task.allow_json
},
"stream": false, // true only if task.allow_stream
"clientId": "anon-uuid" // optional — stable anonymous id for fair rate limiting
}
Response — single-shot
{ "text": "…", "usage": { "promptTokens": 132, "outputTokens": 456 } }
Response — streaming (text/event-stream)
data: {"token":"…"}
data: {"token":"…"}
data: {"done":true}
# mid-stream failure, then the stream closes:
data: {"error":"upstream_error"} # or {"error":"safety_blocked"}
Rate limiting
Two fixed windows per request, both must pass: per client
(task.rate_per_min, bucketed by clientId else IP) and per app
(app.global_rate_per_min). A 429 carries Retry-After: <seconds>.
GET /v1/health
Unauthenticated liveness probe →
{ "ok": true, "service": "…", "ts": "…" }
Error envelope (standardized)
Every non-2xx response has this exact shape — branch on
code, never on message:
{ "error": { "code": "rate_limited", "message": "Rate limit exceeded. Retry later." } }
| Code | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Malformed body / missing required field |
message_required | 400 | message empty after trimming |
unknown_task | 400 | Task not registered for this app (the allowlist said no) |
stream_not_allowed | 400 | stream: true on a task with allow_stream = 0 |
unauthenticated | 401 | Missing bearer token or X-App-Id |
invalid_token | 401 | Token unknown, revoked, or wrong for this app |
task_disabled | 403 | Task exists but is toggled off in this portal |
not_found | 404 | Admin resource does not exist |
safety_blocked | 422 | Provider refused the content |
rate_limited | 429 | Per-client or per-app window exhausted (Retry-After set) |
upstream_error | 502/429 | The provider returned an error |
app_disabled | 503 | The app's proxy toggle is OFF in this portal |
no_active_key | 503 | No active provider key slot for this app |
internal | 500 | Gateway bug — check wrangler tail |
Admin API — everything this portal does, you can curl
All routes require Authorization: Bearer <ADMIN_TOKEN>.
Keys are never returned by any endpoint (only last4); token plaintext is
returned exactly once at creation.
| Method & path | Purpose |
|---|---|
GET /admin/v1/me | Login probe → { ok, role } |
GET /admin/v1/apps | All apps with auth mode, tasks, key slots (last4), tokens (prefix), 24h/7d traffic |
POST /admin/v1/apps | Register an app: { id, name } (id = kebab-case, permanent; starts in token mode) |
PATCH /admin/v1/apps/:id | { enabled } (kill switch) · { auth_mode } · { name } · { global_rate_per_min } |
POST /admin/v1/apps/:id/tokens | Create token (plaintext returned once; stored SHA-256) |
DELETE /admin/v1/apps/:id/tokens/:tokenId | Revoke immediately |
POST /admin/v1/apps/:id/keys | Add a provider key (AES-256-GCM at rest; becomes the active slot) |
PATCH /admin/v1/apps/:id/keys/:slotId | Rotate in place · activate/deactivate |
DELETE /admin/v1/apps/:id/keys/:slotId | Remove the slot |
POST /admin/v1/apps/:id/tasks | Add a task (model, caps, stream/JSON permission, temperature ceiling, rate, safety) |
PATCH /admin/v1/apps/:id/tasks/:task | Update any subset, or { enabled: false } to switch one task off |
GET /admin/v1/stats?appId=&days=7&bucket=day|hour | Time series + per-task breakdown + totals (omit appId for all apps) |
GET /admin/v1/logs?appId=&limit=100 | Recent request rows — sizes, status, tokens, latency. Message content is never stored |
Client integration
Mobile app — public mode (zero build-time credentials)
const res = await fetch('<BASE_URL>/v1/invoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-App-Id': '<app-id>' },
body: JSON.stringify({ task: 'chat', message, history, stream: true }),
});
Server-to-server — token mode (true secret)
const res = await fetch(`${GATEWAY_URL}/v1/invoke`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.AI_GATEWAY_APP_TOKEN}`,
'X-App-Id': '<app-id>',
},
body: JSON.stringify({ task: 'ragAnswer', message: prompt }),
});
Why no token for mobile? Anything bundled in a binary is extractable, so a required token there adds provisioning friction without real security. The protections that matter are server-side and identical either way: the task allowlist, both rate windows, and the kill switch. The provider key never ships. If abuse appears, flip the app to token mode here — no gateway redeploy.