keyward.broker console
control plane offlineoffline
Docs / API

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
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_approval

Denials 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/:provider/*
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&currency=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

RouteWhat
GET /v1/jwkspublic signing keys — verify issued credentials without a shared secret
GET /healthzliveness

Admin surface (session bearer, org-scoped)

RouteRoleWhat
GET /v1/auditviewerrecent tied audit rows
GET /v1/audit/export?format=json|csvviewerfull export, up to 50k rows, page via since=
POST /v1/audit/pruneadmindelete rows older than { days }
GET|POST /v1/agents · /:id/revoke · /:id/restore · /:id/service-keyadminregister (idempotent), revoke, restore; POST/DELETE service-key issues or revokes the agent's kw_svc_… key (returned once)
GET|POST /v1/secrets · /:provider/rotateadminmetadata only — plaintext never returned
GET|POST|DELETE /v1/policiesviewer / admindeny-by-default rules: (agent?, provider?, scope?) → allow | deny | require_approval
GET /v1/approvals · POST /:id/approve|denyviewer / adminhuman-in-the-loop queue for require_approval
GET /v1/admins · POST /:subject/roleownerRBAC management
GET /v1/usage · POST /v1/usage/reportviewer / adminplan, counts, monthly issuance; push to billing
POST /v1/orgs/:id/kmssuperadminBYOK — set the org's customer-managed KMS key
GET /v1/providerspublicbuilt-in provider catalog: scopes, descriptions, routes, key-format hints

Account & tenant auth

RouteWhat
POST /auth/signupcreate a tenant: org + owner account, returns a 12h session token
POST /auth/loginpassword login; 401 totp_required when 2FA is enabled
GET /auth/mecurrent account, org, role, 2FA status
POST /auth/2fa/setup · enable · disableTOTP enrollment (RFC 6238, QR)
GET /auth/tenant?email=tenant SSO routing — returns the org's IdP connector if the email domain has one

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.