Connected Apps / Dynamic Client Registration
Register OAuth 2.0 clients dynamically and manage them via the admin API.
Dynamic Client Registration
POST /oauth2/register
Register a new OAuth 2.0 client following RFC 7591. This endpoint is used by applications that need to become first-class OAuth clients within Bulwark — receiving their own client_id and client_secret — so that agents can authenticate on their behalf.
Headers
X-Bulwark-Tenant: <tenant-id>Authorization: Bearer <initial-access-token>— Optional. Required if DCR is restricted by tenant policy.
Body
{
"client_name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read:customers write:reports",
"token_endpoint_auth_method": "client_secret_basic",
"application_type": "web",
"contacts": ["[email protected]"],
"logo_uri": "https://app.acme.com/logo.png",
"client_uri": "https://app.acme.com"
}
| Field | Required | Description |
|-------|----------|-------------|
| client_name | Yes | Human-readable application name |
| redirect_uris | Yes (for authorization_code) | Allowed redirect URIs |
| grant_types | No | Defaults to ["authorization_code"]. Also supports client_credentials. |
| response_types | No | Defaults to ["code"] |
| scope | No | Requested OAuth scopes |
| token_endpoint_auth_method | No | client_secret_basic, client_secret_post, or none |
Response 201
{
"client_id": "bwk_...",
"client_secret": "...",
"client_name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read:customers write:reports",
"token_endpoint_auth_method": "client_secret_basic",
"client_id_issued_at": 1743292800,
"registration_access_token": "..."
}
Important: client_secret is returned only once. Store it securely.
List Clients (Admin)
GET /api/v1/oauth2/clients
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Response 200
{
"clients": [
{
"id": "01j...",
"client_id": "bwk_...",
"name": "Acme Data Exporter",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "read:customers", "write:reports"],
"is_public": false,
"token_endpoint_auth_method": "client_secret_basic",
"status": "active",
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-03-30T00:00:00Z"
}
],
"count": 1
}
Get Client (Admin)
GET /api/v1/oauth2/clients/{id}
Retrieve a single OAuth 2.0 client by its internal row ID. The tenant is verified server-side — clients that belong to another tenant return 404.
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Path Parameters
| Parameter | Description |
|-----------|-------------|
| id | Internal row ID of the client (the id field, not client_id) |
Response 200
{
"id": "01j...",
"client_id": "bwk_...",
"name": "Acme Data Exporter",
"description": "Exports customer data to the Acme warehouse",
"redirect_uris": ["https://app.acme.com/auth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "read:customers", "write:reports"],
"is_public": false,
"token_endpoint_auth_method": "client_secret_basic",
"status": "active",
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-03-30T00:00:00Z"
}
description is omitted when empty. client_secret is never returned by this endpoint.
Response 404
{ "error": "client not found" }
Create Client (Admin)
POST /api/v1/oauth2/clients
Create a client directly without an initial access token. Equivalent to DCR but requires admin credentials.
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Body
Same schema as POST /oauth2/register.
Response 201
Same schema as the DCR 201 response.
Revoke Client
DELETE /api/v1/oauth2/clients/{id}
Immediately revokes the client and all tokens issued to it. Active sessions authenticated via this client will be terminated.
Headers
Authorization: Bearer <apiKey>X-Bulwark-Tenant: <tenant-id>
Response 200
{
"status": "deleted"
}