HTTP and RSocket API
The SAPL PDP server exposes two network APIs for authorization decisions: HTTP/JSON and RSocket/protobuf. Both offer the same five operations with identical semantics. HTTP is the default and works with any HTTP client. RSocket is an optional high-performance transport using binary protobuf serialization over persistent TCP or Unix domain socket connections.
HTTP API
The HTTP API requires no SDK. Any application that can make HTTP requests can use the PDP.
All endpoints accept POST requests with application/json bodies. Streaming endpoints return text/event-stream (Server-Sent Events); one-shot endpoints return application/json. All endpoints are located under a shared base URL, typically https://<host>:<port>/api/pdp/.
Endpoint Overview
| Endpoint | Method | Response Content-Type | Behavior |
|---|---|---|---|
/api/pdp/decide |
POST | text/event-stream |
Streaming decisions for a single subscription |
/api/pdp/decide-once |
POST | application/json |
One-shot decision for a single subscription |
/api/pdp/multi-decide |
POST | text/event-stream |
Streaming individual decisions for multiple subscriptions |
/api/pdp/multi-decide-all |
POST | text/event-stream |
Streaming batch decisions for multiple subscriptions |
/api/pdp/multi-decide-all-once |
POST | application/json |
One-shot batch decisions for multiple subscriptions |
Authentication
All endpoints require authentication. SAPL Node supports four authentication modes that can be combined:
| Mode | Header | Configuration |
|---|---|---|
| Unauthenticated | (none) | allow-no-auth: true (development only) |
| Basic Auth | Authorization: Basic ... |
allow-basic-auth: true + user entries |
| API Key | Authorization: Bearer sapl_... |
allow-api-key-auth: true + user entries |
| OAuth2 / JWT | Authorization: Bearer <jwt> |
allow-oauth2-auth: true + issuer URI |
Generate credentials with the SAPL CLI:
sapl generate basic --id service-a --pdp-id default
sapl generate apikey --id service-b --pdp-id production
For full authentication configuration, TLS setup, and multi-tenant routing, see Security.
Authorization Subscription Format
A single authorization subscription is a JSON object with three required fields and two optional fields:
{
"subject": {
"username": "alice",
"role": "doctor",
"department": "cardiology"
},
"action": "read",
"resource": {
"type": "patient_record",
"patientId": 123
},
"environment": {
"timestamp": "2025-10-06T14:30:00Z",
"ipAddress": "192.168.1.42"
},
"secrets": {
"jwt": "eyJhbGciOi..."
}
}
- subject (required): Who is making the request. Any JSON value (string, number, object, array, boolean, or null).
- action (required): What operation is being attempted. Any JSON value.
- resource (required): What is being accessed. Any JSON value.
- environment (optional): Additional context such as time, location, or IP address. Any JSON value.
- secrets (optional): Sensitive data for Policy Information Points (tokens, API keys, credentials). Any JSON value. Not included in logs or traces.
For the full subscription format, see Authorization Subscriptions.
Authorization Decision Format
Every endpoint returns authorization decisions as JSON objects:
{
"decision": "PERMIT",
"obligations": [
{
"type": "log_access",
"message": "Patient record accessed by alice"
}
],
"advice": [
{
"type": "notify",
"channel": "audit"
}
],
"resource": {
"type": "patient_record",
"patientId": 123,
"name": "***REDACTED***"
}
}
- decision (always present): One of
PERMIT,DENY,INDETERMINATE, orNOT_APPLICABLE. - obligations (optional): An array of JSON objects. Instructions the PEP must enforce before granting access. If a PEP cannot fulfill any obligation, it must deny access regardless of the decision.
- advice (optional): An array of JSON objects. Suggestions the PEP should follow but may ignore without affecting the authorization outcome.
- resource (optional): A JSON value that replaces the original resource data (e.g., with fields redacted or transformed).
A minimal decision contains only the decision field:
{
"decision": "DENY"
}
For details on how PEPs must handle obligations and advice, see Authorization Decisions.
Single Subscription Endpoints
Decide (Streaming)
POST {baseURL}/decide
Content-Type: application/json
Accept: text/event-stream
Returns an initial decision, then pushes updated decisions whenever policies, attributes, or conditions change. Each SSE event contains a complete authorization decision in its data field. The server may send SSE comment events (: keep-alive) to keep the connection alive. The client must close the connection to stop receiving updates.
Request body:
{
"subject": "alice",
"action": "read",
"resource": "document"
}
Response (Server-Sent Events, one event per decision change):
data: {"decision":"PERMIT"}
data: {"decision":"DENY","obligations":[{"type":"log_access","reason":"policy changed"}]}
: keep-alive
Example with curl:
curl -N -X POST https://localhost:8443/api/pdp/decide \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sapl_..." \
-d '{"subject":"alice","action":"read","resource":"document"}'
Example with the SAPL CLI (streams decisions as NDJSON):
sapl decide --remote --url https://localhost:8443 --token sapl_... \
-s '"alice"' -a '"read"' -r '"document"'
Decide Once (One-Shot)
POST {baseURL}/decide-once
Content-Type: application/json
Accept: application/json
Returns a single authorization decision and closes the connection. Use this for request-response scenarios where continuous updates are not needed.
Request body:
{
"subject": { "username": "alice", "role": "doctor" },
"action": "read",
"resource": { "type": "patient_record", "patientId": 123 }
}
Response:
{
"decision": "PERMIT",
"obligations": [
{
"type": "log_access",
"message": "Patient record accessed"
}
]
}
Example with the SAPL CLI:
sapl decide-once --remote --url https://localhost:8443 --token sapl_... \
-s '{"username":"alice","role":"doctor"}' -a '"read"' -r '{"type":"patient_record","patientId":123}'
The sapl check command returns an exit code instead of JSON output, making it suitable for shell scripts and CI/CD pipelines:
sapl check --remote --url https://localhost:8443 --token sapl_... \
-s '"alice"' -a '"read"' -r '"document"' && echo "PERMIT"
For the full CLI reference, see Command Line.
Multi-Subscription Endpoints
Multi-subscriptions bundle multiple authorization subscriptions into a single request. This is useful when a PEP needs to evaluate several authorization questions at once, for example when rendering a UI that shows multiple resources with different access levels.
A multi-subscription is a JSON object mapping client-chosen subscription IDs to individual authorization subscriptions:
{
"read-patient-record": {
"subject": { "username": "alice", "role": "doctor" },
"action": "read",
"resource": { "type": "patient_record", "patientId": 123 }
},
"write-clinical-notes": {
"subject": { "username": "alice", "role": "doctor" },
"action": "write",
"resource": { "type": "clinical_notes", "patientId": 123 }
},
"delete-audit-log": {
"subject": { "username": "alice", "role": "doctor" },
"action": "delete",
"resource": { "type": "audit_log" }
}
}
Each key is a unique subscription ID chosen by the PEP. Each value is a standard authorization subscription with subject, action, resource, and optionally environment and secrets.
Multi Decide (Streaming Individual)
POST {baseURL}/multi-decide
Content-Type: application/json
Accept: text/event-stream
Returns individual decisions as they change, each tagged with its subscription ID. Only subscriptions whose decisions actually changed emit updates. This is efficient when most decisions remain stable.
Response (Server-Sent Events, one event per changed decision):
data: {"subscriptionId":"read-patient-record","decision":{"decision":"PERMIT"}}
data: {"subscriptionId":"write-clinical-notes","decision":{"decision":"PERMIT","obligations":[{"type":"log_access"}]}}
data: {"subscriptionId":"delete-audit-log","decision":{"decision":"DENY"}}
data: {"subscriptionId":"write-clinical-notes","decision":{"decision":"DENY"}}
Each event contains a subscriptionId identifying which subscription the decision belongs to, and a decision object with the authorization decision including any obligations, advice, or resource transformations.
Multi Decide All (Streaming Batch)
POST {baseURL}/multi-decide-all
Content-Type: application/json
Accept: text/event-stream
Returns all decisions as a single object whenever any decision changes. Each message contains the complete current state of all decisions.
Response (Server-Sent Events, one event per change to any decision):
data: {"read-patient-record":{"decision":"PERMIT"},"write-clinical-notes":{"decision":"PERMIT","obligations":[{"type":"log_access"}]},"delete-audit-log":{"decision":"DENY"}}
data: {"read-patient-record":{"decision":"PERMIT"},"write-clinical-notes":{"decision":"DENY"},"delete-audit-log":{"decision":"DENY"}}
This format is simpler to process than individual updates because each message is a complete snapshot. The trade-off is that every message repeats all decisions, even those that have not changed.
Multi Decide All Once (One-Shot Batch)
POST {baseURL}/multi-decide-all-once
Content-Type: application/json
Accept: application/json
Returns all decisions as a single JSON object and closes the connection. The format is identical to the streaming batch endpoint, but the connection closes after the first response.
Response:
{
"read-patient-record": {
"decision": "PERMIT"
},
"write-clinical-notes": {
"decision": "PERMIT",
"obligations": [
{
"type": "log_access",
"message": "Clinical notes accessed"
}
]
},
"delete-audit-log": {
"decision": "DENY"
}
}
All multi-subscription decisions may include optional resource, obligations, and advice fields, as described in Authorization Decisions.
Error Handling
A PEP encountering connectivity issues or errors with the PDP server must treat this as an INDETERMINATE decision and deny access. The PEP should reconnect using an exponential backoff strategy to avoid overloading the PDP.
Keep-Alive
Streaming connections use periodic SSE comment events (: keep-alive) to prevent firewalls and proxies from closing idle connections. A PEP should treat a prolonged absence of any events (decisions or keep-alives) as a connection failure.
Reverse Proxy Configuration
The streaming endpoints (/api/pdp/decide, /api/pdp/multi-decide, /api/pdp/multi-decide-all) use SSE over long-lived HTTP POST connections. Default proxy configurations buffer responses and time out idle connections, both of which break SSE streaming.
Requirements for any reverse proxy in front of SAPL Node:
- Disable response buffering. SSE events must be flushed immediately.
- Set a long read timeout. Streaming connections stay open indefinitely.
- Preserve chunked transfer encoding. Do not add
Content-Lengthheaders to streaming responses. - Forward the HTTP method. All PDP endpoints use POST.
SAPL Node can send periodic keep-alive frames on idle connections:
io.sapl.node:
keep-alive: 15
Set the proxy read timeout above this interval (e.g., 60 seconds). See Configuration for the property reference.
nginx
location /api/pdp/ {
proxy_pass http://127.0.0.1:8443;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
}
location /actuator/ {
proxy_pass http://127.0.0.1:8443;
}
Apache
Enable mod_proxy and mod_proxy_http. Disable response buffering for the PDP path:
ProxyPass /api/pdp/ http://127.0.0.1:8443/api/pdp/
ProxyPassReverse /api/pdp/ http://127.0.0.1:8443/api/pdp/
SetEnv proxy-sendchunked 1
SetEnv proxy-sendcl 0
ProxyTimeout 3600
ProxyPass /actuator/ http://127.0.0.1:8443/actuator/
ProxyPassReverse /actuator/ http://127.0.0.1:8443/actuator/
The one-shot endpoints (/api/pdp/decide-once, /api/pdp/multi-decide-all-once) and actuator endpoints work with default proxy settings.
Server Implementation
The SAPL Policy Engine ships with SAPL Node, a standalone PDP server. SAPL Node supports filesystem directories, signed bundles, and remote bundle fetching as policy sources. It is available as a Docker container and as a native binary. See SAPL Node for deployment and configuration.
RSocket API
The RSocket API provides the same five operations as HTTP using protobuf serialization over persistent TCP or Unix domain socket (UDS) connections. It is significantly faster than HTTP/JSON for high-throughput workloads. RSocket is disabled by default. For server configuration, see Configuration.
Wire Format
Each RSocket payload has two parts:
- Metadata: the route name as a UTF-8 string (e.g.,
"decide-once") - Data: the protobuf-encoded request or response message
No composite metadata, no MIME type negotiation. The route string alone determines the operation.
Protobuf Specification
The wire format is defined by two .proto files shipped in the sapl-api-proto module. These are the platform-independent specification. Any language with a protobuf compiler and an RSocket client library can build a compatible client from them.
Service Definition (sapl_service.proto)
service PolicyDecisionPointService {
rpc Decide(AuthorizationSubscription) returns (stream AuthorizationDecision);
rpc DecideOnce(AuthorizationSubscription) returns (AuthorizationDecision);
rpc MultiDecide(MultiAuthorizationSubscription) returns (stream IdentifiableAuthorizationDecision);
rpc MultiDecideAll(MultiAuthorizationSubscription) returns (stream MultiAuthorizationDecision);
rpc MultiDecideAllOnce(MultiAuthorizationSubscription) returns (MultiAuthorizationDecision);
}
Message Definitions (sapl_types.proto)
message AuthorizationSubscription {
Value subject = 1;
Value action = 2;
Value resource = 3;
Value environment = 4;
Value secrets = 5;
}
message AuthorizationDecision {
Decision decision = 1;
ArrayValue obligations = 2;
ArrayValue advice = 3;
Value resource = 4;
}
enum Decision {
INDETERMINATE = 0;
PERMIT = 1;
DENY = 2;
NOT_APPLICABLE = 3;
}
message Value {
oneof kind {
NullValue null_value = 1;
bool bool_value = 2;
string number_value = 3; // BigDecimal as string for precision
string text_value = 4;
ArrayValue array_value = 5;
ObjectValue object_value = 6;
bool undefined_value = 7;
ErrorValue error_value = 8;
}
}
message MultiAuthorizationSubscription {
repeated IdentifiableAuthorizationSubscription subscriptions = 1;
}
message MultiAuthorizationDecision {
map<string, AuthorizationDecision> decisions = 1;
}
Numbers are encoded as decimal strings to preserve arbitrary precision. INDETERMINATE is enum value 0 so that uninitialized proto3 fields default to the safe denial state.
Operations
| Route | RSocket Pattern | Request | Response |
|---|---|---|---|
decide |
Request-Stream | AuthorizationSubscription |
AuthorizationDecision (stream) |
decide-once |
Request-Response | AuthorizationSubscription |
AuthorizationDecision |
multi-decide |
Request-Stream | MultiAuthorizationSubscription |
IdentifiableAuthorizationDecision (stream) |
multi-decide-all |
Request-Stream | MultiAuthorizationSubscription |
MultiAuthorizationDecision (stream) |
multi-decide-all-once |
Request-Response | MultiAuthorizationSubscription |
MultiAuthorizationDecision |
Streaming operations push updated decisions whenever policies, attributes, or context change. Unlike HTTP SSE, RSocket streams support native backpressure.
Authentication
Authentication is performed once during the RSocket connection setup frame, not per request. Credentials are encoded in the setup frame’s metadata using the RSocket authentication metadata extension.
| Method | Metadata Encoding |
|---|---|
| Basic Auth | AuthMetadataCodec.encodeSimpleMetadata() |
| API Key / Bearer Token | AuthMetadataCodec.encodeBearerMetadata() |
If authentication fails, the server rejects the setup with a REJECTED_SETUP error frame.
Connection Lifecycle
RSocket connections are persistent. Connection lifetime is bounded by credential expiry (JWT exp claim) and an optional server-configured maximum. The effective lifetime is the minimum of these two bounds. Expired connections are disposed by the server; clients must reconnect.
Error Handling
All operations return INDETERMINATE on unparseable requests, encoding failures, or unknown routes. This matches the HTTP API’s fail-safe behavior.
Comparison
| Aspect | HTTP | RSocket |
|---|---|---|
| Serialization | JSON | Protobuf |
| Streaming | Server-Sent Events | Native RSocket streams |
| Connection | Per-request or multiplexed | Persistent TCP or UDS |
| Authentication | Per-request HTTP headers | Once at connection setup |
| Backpressure | None (SSE) | Native flow control |
| Interoperability | Any HTTP client | Requires RSocket + protobuf library |