{"openapi":"3.1.0","info":{"title":"Vertex Control Plane API","version":"2026-05-27","description":"Vertex is a zero-trust control plane for agent API access. Management endpoints take a terminal OAuth device session (vtx_device_*, from `vertex login`) — there is no standing masterkey. They mint scoped keychains, list services, and read plan/usage/activity. The data plane (POST /api/proxy) and keychain self-service use a vtx_session_* keychain, documented separately.","contact":{"email":"support@vertex.com"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://www.vertex.blue"}],"security":[{"vertexBearer":[]}],"components":{"securitySchemes":{"vertexBearer":{"type":"http","scheme":"bearer","bearerFormat":"vtx_device_*","description":"Terminal OAuth device session (`Authorization: Bearer vtx_device_*`) — the management-plane identity from `vertex login`. Short-lived (~120m); re-run the login when it expires. The dashboard browser session is the equivalent management credential. Minting a keychain requires this OAuth identity; there is no standing token that can mint."},"keychainBearer":{"type":"http","scheme":"bearer","bearerFormat":"vtx_session_*","description":"An agent's own scoped keychain (`Authorization: Bearer vtx_session_*`) — the same token it sends to the proxy. Used by keychain self-service routes so an agent can introspect and revoke itself without a management session. The `x-vertex-token` header (keychainHeader) is accepted equivalently."},"keychainHeader":{"type":"apiKey","in":"header","name":"x-vertex-token","description":"The same scoped keychain (`vtx_session_*`) sent via the `x-vertex-token` header instead of `Authorization: Bearer` — identical to the header the agent already sends to POST /api/proxy, so the data plane and self-service routes take one consistent header."}},"schemas":{"ErrorCode":{"type":"string","description":"Stable, machine-readable error code returned in the `error` field of a 4xx/5xx JSON body. New codes may be added over time; treat unknown codes as a generic failure of the same HTTP class.","enum":["missing_bearer","invalid_api_key_format","unauthorized","forbidden","rate_limited","invalid_json","invalid_request","not_found","authorization_unavailable","issuance_failed","policy_cache_unavailable","plan_keychain_limit_reached"]},"ProxyDenialCode":{"type":"string","description":"Data-plane denial code returned in the `error` field when POST /api/proxy refuses to forward a request. Each code maps to exactly one policy control, so an agent can branch on it (e.g. back off on session_rate_limited, surface a budget error on session_spend_limit_denied, re-mint on session_token_revoked_or_expired) instead of treating every refusal the same. New codes may be added over time; treat unknown codes as a generic policy denial.","enum":["session_rate_limited","session_domain_denied","session_method_denied","session_tool_denied","session_semantic_policy_denied","session_spend_limit_denied","session_workspace_frozen","session_token_malformed","session_token_revoked_or_expired","session_token_lifetime_denied","session_policy_storage_failed","session_policy_cache_unavailable"]},"Error":{"type":"object","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/ErrorCode"},"detail":{"type":"string","description":"Optional human-readable explanation"},"retryAfterSeconds":{"type":"integer","description":"Present on 429 (rate_limited) responses. Seconds to wait before retrying."}},"example":{"error":"plan_keychain_limit_reached","detail":"Free plan allows 1 active keychain. Revoke an existing keychain or upgrade."}},"RateLimitedError":{"type":"object","description":"429 response body. The same shape is returned for per-IP and per-session rate limits.","required":["error","retryAfterSeconds"],"properties":{"error":{"type":"string","enum":["rate_limited"]},"retryAfterSeconds":{"type":"integer","description":"Seconds until the fixed window resets and the caller may retry."}},"example":{"error":"rate_limited","retryAfterSeconds":42}},"Plan":{"type":"object","properties":{"id":{"type":"string","enum":["free","developer","business"]},"name":{"type":"string"},"price":{"type":"object","properties":{"cents":{"type":"integer"},"currency":{"type":"string"},"interval":{"type":"string"}}},"limits":{"type":"object","properties":{"stored_keys":{"type":"integer","nullable":true,"description":"null = unlimited"},"active_keychains":{"type":"integer"},"seats":{"type":"integer"},"environments":{"type":"integer"},"monthly_calls":{"type":"integer"},"audit_retention_days":{"type":"integer"},"max_key_spend_cents":{"type":"integer"},"max_key_single_amount_cents":{"type":"integer"},"pilot_messages_per_day":{"type":"integer"}}},"support":{"type":"string"},"features":{"type":"array","items":{"type":"string"}}}},"PlanLimits":{"type":"object","description":"Effective server-side caps for the bearer's current plan. Use these to pre-validate a KeychainRequest before issuance; the keychain route clamps requested policy values to these bounds rather than rejecting them.","properties":{"max_keychain_ttl_minutes":{"type":"integer","description":"Maximum keychain ttl_minutes accepted at issuance (hard ceiling 1440; requests above are clamped down)."},"min_keychain_ttl_minutes":{"type":"integer","description":"Minimum keychain ttl_minutes (requests below are clamped up)."},"max_keychain_requests_per_minute":{"type":"integer","description":"Maximum keychain max_requests_per_minute accepted at issuance (clamped)."},"max_spend_cents":{"type":"integer","description":"Absolute upper bound for a keychain's daily spend wallet. There is no per-plan ceiling — this is the int4 maximum every value is clamped to."},"max_single_amount_cents":{"type":"integer","description":"Absolute upper bound for a keychain's per-transaction limit. There is no per-plan ceiling — this is the int4 maximum every value is clamped to."},"active_keychains":{"type":"integer","description":"Maximum number of concurrently active keychains; issuance fails with plan_keychain_limit_reached once reached."},"monthly_calls":{"type":"integer","description":"Hard monthly cap on routed proxy calls for this plan."}}},"KeychainPolicy":{"type":"object","properties":{"ttl_minutes":{"type":"integer","minimum":5,"maximum":1440,"description":"Keychain lifetime in minutes. Server-side clamped to [5, 1440] (values below 5 are raised to 5, above 1440 lowered to 1440) rather than rejected."},"max_requests_per_minute":{"type":"integer","minimum":1,"maximum":600,"description":"Per-keychain request rate. Server-side clamped to [1, 600] rather than rejected."},"max_spend_cents":{"type":"integer","minimum":0,"maximum":100000000,"description":"Daily spend wallet (cents). Total the keychain can spend per UTC day across all actions; resets at 00:00 UTC. No plan ceiling — defaults to 50000 ($500) and is clamped only to the int4 maximum."},"max_single_amount_cents":{"type":"integer","minimum":0,"maximum":100000000,"description":"Per-transaction limit (cents). Largest amount allowed in any single action (e.g. one charge or payout). No plan ceiling — defaults to 50000 ($500) and is clamped only to the int4 maximum."},"allowed_tools":{"type":"array","items":{"type":"string"}},"denied_tools":{"type":"array","items":{"type":"string"}}}},"KeychainRequest":{"type":"object","required":["agent_name"],"properties":{"agent_name":{"type":"string","minLength":2,"description":"Human-readable name for the keychain/agent."},"service_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"UUIDs of vaulted services to scope the keychain to (list them via GET /api/v1/services). May be empty for a keychain with no service access yet."},"allowed_methods":{"type":"array","items":{"type":"string"}},"policy":{"$ref":"#/components/schemas/KeychainPolicy"}}},"IssuedKeychain":{"type":"object","required":["keychain","session_id","policy_id","expires_at","proxy_endpoint"],"properties":{"keychain":{"type":"string","description":"vtx_session_* token. Shown once; not retrievable again."},"session_id":{"type":"string","format":"uuid"},"policy_id":{"type":"string","format":"uuid"},"expires_at":{"type":"string","format":"date-time"},"proxy_endpoint":{"type":"string","format":"uri","description":"Send the agent's real API calls here as the Authorization bearer (this keychain). Vertex enforces policy and forwards to the provider. On a policy denial the response body's `error` field is a stable ProxyDenialCode (see #/components/schemas/ProxyDenialCode) the agent can branch on."}}},"Service":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"kind":{"type":"string"},"base_url":{"type":"string","format":"uri"},"auth_scheme":{"type":"string"},"allowed_methods":{"type":"array","items":{"type":"string"}},"scopes":{"type":"array","items":{"type":"string"}},"tracks_spend":{"type":"boolean","description":"Whether this key moves money (drives spend metering + the dashboard $ bar)."},"created_at":{"type":"string","format":"date-time"}}}}},"paths":{"/api/auth/google/device":{"post":{"summary":"Start Google device-code signup for an outside agent","security":[],"responses":{"201":{"description":"Device authorization started","content":{"application/json":{"schema":{"type":"object","properties":{"device_id":{"type":"string","format":"uuid"},"user_code":{"type":"string"},"verification_url":{"type":"string","format":"uri"},"expires_in":{"type":"integer"},"interval":{"type":"integer"}}}}}}}}},"/api/auth/google/device/poll":{"post":{"summary":"Poll a Google device-code login until it returns a workspace and one-time device session","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device_id"],"properties":{"device_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Workspace bootstrapped. The device_session.token field is shown once.","content":{"application/json":{"schema":{"type":"object"}}}},"202":{"description":"pending or slow_down","content":{"application/json":{"schema":{"type":"object"}}}},"410":{"description":"expired, denied, or already consumed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/health":{"get":{"summary":"Liveness probe + API version","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/session":{"get":{"summary":"Introspect the calling agent's own keychain — limits, live remaining spend, accessible services, expiry","description":"Authenticated by the agent's own vtx_session_* keychain (keychainBearer), not a management session. Lets an agent see the policy the proxy enforces so it can self-correct (back off before a rate/spend denial, skip a service it can't reach) instead of learning its limits only by being blocked.","security":[{"keychainBearer":[]},{"keychainHeader":[]}],"responses":{"200":{"description":"The keychain's policy as enforced by the proxy","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"agent_name":{"type":"string"},"status":{"type":"string","enum":["active"]},"expires_at":{"type":"string","format":"date-time"},"proxy_endpoint":{"type":"string","format":"uri"},"limits":{"type":"object","properties":{"max_requests_per_minute":{"type":"integer"},"max_spend_cents":{"type":"integer"},"max_single_amount_cents":{"type":"integer"}}},"spend":{"type":"object","properties":{"spent_cents":{"type":"integer"},"remaining_cents":{"type":"integer","nullable":true,"description":"null when no spend cap is set"}}},"allowed_methods":{"type":"array","items":{"type":"string"}},"allowed_tools":{"type":"array","items":{"type":"string"}},"denied_tools":{"type":"array","items":{"type":"string"}},"accessible_services":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string"},"status":{"type":"string"}}}}}}}}},"401":{"description":"Missing, malformed, revoked, or expired keychain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Self-revoke the calling agent's keychain","description":"Lets a finished or compromised agent retire its own credential immediately, without a management session. Flips the session to revoked and clears the proxy's policy cache so the next call is denied. Idempotent.","security":[{"keychainBearer":[]},{"keychainHeader":[]}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["revoked"]},"revoked":{"type":"boolean"}}}}}},"401":{"description":"Missing, malformed, revoked, or expired keychain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/plans":{"get":{"summary":"Public plan catalog","security":[],"responses":{"200":{"description":"Plan list","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Plan"}}}}}}}}}},"/api/v1/me":{"get":{"summary":"Resolve the workspace + calling principal (device session), plus the plan's effective limits","responses":{"200":{"description":"Workspace identity","content":{"application/json":{"schema":{"type":"object","properties":{"workspace":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"plan":{"type":"string","enum":["free","developer","business"]}}},"principal":{"type":"object","properties":{"type":{"type":"string","enum":["device_session"]},"user_id":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["owner","admin","developer","auditor"]}}},"plan_limits":{"$ref":"#/components/schemas/PlanLimits"}}},"example":{"workspace":{"id":"11111111-1111-1111-1111-111111111111","slug":"acme","plan":"developer"},"principal":{"type":"device_session","user_id":"22222222-2222-2222-2222-222222222222","role":"developer"},"plan_limits":{"max_keychain_ttl_minutes":1440,"min_keychain_ttl_minutes":5,"max_keychain_requests_per_minute":600,"max_spend_cents":2147483647,"max_single_amount_cents":2147483647,"active_keychains":200,"monthly_calls":100000}}}}},"401":{"description":"Missing or invalid bearer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized"}}}}}}},"/api/v1/activity":{"get":{"summary":"Recent agent activity for the workspace — the live feed the dashboard + homepage render","description":"Who (agent), what (service · tool), when, allowed/blocked, a spend bar, and headline metrics (calls today, blocked, secrets exposed). Identical derivation to the dashboard, so the API, dashboard, and `vertex activity` CLI always agree. Requires a vtx_device_* session.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":12},"description":"Rows to return (default 12, max 50)."}],"responses":{"200":{"description":"Activity feed","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["activity_feed"]},"keychain_count":{"type":"integer"},"metrics":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"value":{"type":"string"}}}},"rows":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"agent":{"type":"string"},"target":{"type":"string"},"time":{"type":"string","description":"HH:MM (UTC)"},"status":{"type":"string","enum":["allowed","blocked","failed"]},"reason":{"type":"string"},"spend":{"type":"object","properties":{"used":{"type":"integer"},"cap":{"type":"integer"}}}}}}}}}}}}}},"/api/v1/usage":{"get":{"summary":"Current month's routed-call count vs the plan quota","responses":{"200":{"description":"Usage snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string","description":"YYYY-MM (UTC)"},"used":{"type":"integer"},"quota":{"type":"integer"},"percent_used":{"type":"number"}}}}}}}}},"/api/v1/services":{"get":{"summary":"List provider credentials vaulted in the workspace (to scope a keychain)","description":"Metadata only; the vault secret is never returned. Vaulting or rotating a provider secret is a human, member-authorized action in the dashboard — deliberately not on the agent API.","responses":{"200":{"description":"Service list","content":{"application/json":{"schema":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}}}}}}},"/api/v1/keychains":{"post":{"summary":"Mint a vtx_session_* keychain in the workspace (device session)","description":"Minting requires the OAuth device session (or dashboard) — there is no standing token that can mint, so a leaked keychain can never make more keychains. Headless multi-tenant minting (per-end-user keychains tagged with a tenant id) is deferred to a future scoped machine identity.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeychainRequest"},"example":{"agent_name":"support-copilot","service_ids":["33333333-3333-3333-3333-333333333333"],"allowed_methods":["GET","POST"],"policy":{"max_requests_per_minute":60,"max_spend_cents":50000,"max_single_amount_cents":50000,"allowed_tools":[],"denied_tools":[]}}}}},"responses":{"201":{"description":"Issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssuedKeychain"},"example":{"keychain":"vtx_session_live_xxxxxxxxxxxxxxxx","session_id":"44444444-4444-4444-4444-444444444444","policy_id":"55555555-5555-5555-5555-555555555555","expires_at":"2026-05-29T13:00:00.000Z","proxy_endpoint":"https://www.vertex.blue/api/proxy"}}}},"402":{"description":"plan_keychain_limit_reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"plan_keychain_limit_reached","detail":"Free plan allows 1 active keychain. Revoke an existing keychain or upgrade."}}}},"429":{"description":"rate_limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}}}}},"/api/v1/ask":{"post":{"summary":"Ask Pilot (the Vertex copilot) a question (device session)","description":"Stateless, tool-less Q&A for developers and the agents they run — scoping keychains, designing policy, reading the audit trail, debugging denials. Takes NO actions and persists nothing, so it is safe on the management plane. The same Pilot copilot as the dashboard, metered by the same per-tier daily Pilot quota (Free 25 / Developer 250 / Business 500 messages a day).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","maxLength":8000,"description":"The question. Treated purely as data — the copilot takes no instructions from it."}}},"example":{"prompt":"How do I scope a keychain to read-only Stripe?"}}}},"responses":{"200":{"description":"Answer","content":{"application/json":{"schema":{"type":"object","properties":{"answer":{"type":"string"},"usage":{"type":"object","properties":{"promptTokens":{"type":"integer"},"completionTokens":{"type":"integer"},"model":{"type":"string"}}},"quota":{"type":"object","properties":{"used":{"type":"integer"},"limit":{"type":"integer"},"remaining":{"type":"integer"},"resets_in_seconds":{"type":"integer"}}}}}}}},"429":{"description":"pilot_quota_exceeded — daily per-tier Pilot cap reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"pilot_not_configured — Pilot backend or rate-limit store unavailable (fail-closed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}