Control-plane REST API
Two audiences: agents hit the broker/gateway with a service key (or an end-user token in delegated mode); humans hit the admin surface with a verified session (or the dashboard does it for them). All admin routes are org-scoped and role-gated: viewer < admin < owner < superadmin.
The broker
POST /v1/credentials
Authorization: Bearer <end-user identity token> # end-user identity token (delegated mode)
X-Keyward-Agent-Key: kw_svc_… # OR: service-agent mode — no user token needed
X-Keyward-Agent: invoice-bot
X-Keyward-Environment: prod # optional, default prod
X-Keyward-Agent-Assertion: <oidc jwt> # required only for attested agents
X-Keyward-Delegation: <prior keyward credential> # chain extension (replaces user token)
{ "provider": "stripe", "scope": "charges:read", "ttl": "5m" }
→ 200 { credential, token_type, provider, scope, expires_at, delegation }
→ 202 { status: "pending_approval", approval_id } # policy effect require_approvalDenials are explicit and every one is audited: 401 invalid_user_token / invalid_delegation, 404 unknown_agent / unknown_provider, 403 agent_revoked / scope_not_allowed / policy_denied / agent_attestation_required / delegation_too_deep, 502 native_mint_failed. Every reason is catalogued with its fix in Troubleshooting.
The gateway (proxy mode)
The strongest form of the wedge: the agent holds no upstream key at all. It calls the provider's API through Keyward, which injects the real secret server-side, forwards the request, and audits what actually happened. Ideal for static-bearer SaaS APIs (Stripe, OpenAI…) that have no short-lived-token mechanism.
POST /v1/gateway/stripe/v1/charges
Authorization: Bearer <end-user identity token> # end-user identity token (delegated mode)
X-Keyward-Agent-Key: kw_svc_… # OR: service-agent mode — no user token needed
X-Keyward-Agent: invoice-bot
X-Keyward-Environment: prod # optional, default prod
amount=2000¤cy=usd&source=tok_visa
# Keyward → POST https://api.stripe.com/v1/charges
# with Authorization: Bearer <the real sk_live_… injected server-side>
→ 200 (Stripe's response, verbatim; the agent never sees the key)Keyward maps the request's (method, path) to a scope, enforces the secret's scope allowlist and policy, then proxies. Denials mirror the broker plus 400 gateway_provider_unsupported, 403 route_not_allowed, 400 native_not_proxyable, 502 upstream_unreachable — including 403 { error: "approval_required", approval_id } when a require_approval rule matches: the call queues the approval and the SAME call, retried after an admin approves, consumes it and proceeds to the upstream. From the SDK: await kw.gateway("stripe", "/v1/charges") returns the upstream Response. Every reason is catalogued with its fix in Troubleshooting.
14 built-in providers — stripe, openai, anthropic, github, sendgrid, slack, notion, airtable, discord, twilio, cloudflare, neon, vercel, supabase — each with a verified route→scope map; the full catalog (every scope, description, and route) is in the Provider reference, served live from GET /v1/providers (public, static metadata). For anything else, choose Custom API and supply a base URL, auth header, and value prefix; the broker proxies any path under it, gated by one scope. Stored per-secret as gateway_config.
Verification & health
Admin surface (session bearer, org-scoped)
Account & tenant auth
Provisioning (enterprise)
SCIM 2.0 Users under /scim/v2/Users (Okta / Entra provisioning; static bearer via KEYWARD_SCIM_TOKEN, disabled when unset). Audit events stream in real time to a SIEM webhook when configured.