Configure an SSF and CAEP receiver stream
Configure a receiver stream, promote it so inbound Security Event Tokens are accepted, and confirm a delivery landed.
Scope
PasskeyBridge receives Security Event Tokens from an upstream transmitter and, when an event maps to a hard signal, runs the revocation cascade inside the same request. This page covers configuring a receiver stream, promoting it so inbound tokens are accepted, what the endpoint returns, and how to confirm a delivery landed.
A token is accepted only against a receiver stream whose status is verified. A newly created stream is written as enabled, so promoting it is a deliberate step you take once the issuer and JWKS URL are right. Step 2 shows the call.
For the full reference see SSF and CAEP shared signals; for the outbound direction, where PasskeyBridge signs and delivers events to your own receivers, see CAEP outbound transmitter; for the reasoning see the shared signals article.
Prerequisites
- Admin of the organization. Every stream action except
list_streamsandlist_eventsrequires it; those two require membership. There is no API-key lane on this function and nossescope. - Your organization id, from Developer > Settings, field Organization ID.
- An upstream transmitter that publishes a
/.well-known/ssf-configurationdocument, its issuer URI, and the JWKS URI its tokens are signed against. - The CAEP event types that transmitter emits.
Calls go to https://api.passkeybridge.io/v1/shield-sse-caep with a session token in Authorization: Bearer. The action is a field in the JSON body rather than a path segment.
Step 1 · Discover the upstream transmitter
Fetch the upstream configuration to confirm it speaks SSF and to collect the values PasskeyBridge pins against:
curl https://idp.example.com/.well-known/ssf-configurationNote the issuer (the iss claim its tokens carry) and the jwks_uri (where its signing keys are published). PasskeyBridge matches an inbound token to a stream by iss and verifies the signature against the stream's stored JWKS URL, so both values must be exact.
Where the vendor puts its stream configuration UI is vendor-specific: Okta under Security and Shared Signals, Entra ID under its continuous access evaluation settings, Auth0 in tenant settings.
Step 2 · Configure the receiver stream
Create the stream with configure_stream:
POST https://api.passkeybridge.io/v1/shield-sse-caep
Content-Type: application/json
Authorization: Bearer YOUR_ADMIN_SESSION_TOKEN{
"action": "configure_stream",
"tenant_id": "YOUR_ORGANIZATION_ID",
"config": {
"stream_uri": "https://idp.example.com/.well-known/ssf-configuration",
"direction": "receiver",
"delivery_method": "push",
"issuer": "https://idp.example.com",
"jwks_url": "https://idp.example.com/.well-known/jwks.json",
"audience": "https://api.passkeybridge.io",
"events_requested": [
"https://schemas.openid.net/secevent/caep/event-type/session-revoked",
"https://schemas.openid.net/secevent/caep/event-type/credential-change",
"https://schemas.openid.net/secevent/caep/event-type/assurance-level-change"
]
}
}Fields. stream_uri and direction are required; direction must be receiver or transmitter. delivery_method defaults to push. issuer, jwks_url and audience default to null, and a receiver stream needs issuer and jwks_url to be usable at all. audience is checked only when you set it. events_requested is filtered against the CAEP event URIs the platform knows, and whatever survives is also copied into events_delivered.
Response (200):
{ "id": "6f2c...", "status": "configured" }status in that response describes the call rather than the row. The row is written with status: "enabled", and inbound receipt requires verified.
Promote the stream. Once issuer and jwks_url match what the upstream publishes, set the row to verified with update_stream:
{
"action": "update_stream",
"tenant_id": "YOUR_ORGANIZATION_ID",
"stream_id": "6f2c...",
"status": "verified"
}It answers { "status": "updated" } and writes an sse.stream.update audit entry. Promotion is an assertion by an admin that this issuer is one you trust: nothing contacts the upstream on your behalf, and verify_stream records a local handshake attempt without changing status. Set it back to enabled, paused or disabled the same way to stop accepting tokens.
List what exists: {"action": "list_streams", "tenant_id": "YOUR_ORGANIZATION_ID"} returns each stream with its direction, delivery_method, events_requested, events_delivered, issuer, audience, status, last_verified_at and timestamps. update_stream (with stream_id) can change delivery_endpoint_url, events_requested, events_delivered, issuer, audience, jwks_url and status; delete_stream removes it, leaving its events behind with the stream reference cleared.
Step 3 · The inbound endpoint
The receiver endpoint is the same function, with action: "receive_set". It takes a JSON body rather than a bare application/secevent+jwt payload, so a transmitter that posts the raw token needs a small relay in front of it to wrap the token in this envelope:
curl -X POST https://api.passkeybridge.io/v1/shield-sse-caep -H "Content-Type: application/json" -d '{
"action": "receive_set",
"tenant_id": "YOUR_ORGANIZATION_ID",
"set_jwt": "eyJhbGciOiJFUzI1NiIsImtpZCI6Imtl..."
}'The organization id may instead be sent as the x-pb-tenant-id header. No session token is needed: the signature is the authorization.
On a verified stream, in order: the signature is checked against the stream's JWKS (ES256, ES384 or RS256 only, alg must agree with the key type, none and unknown critical headers refused), then iss and, when configured, aud; the structural claims iss, iat, jti and events must be present, with iat no more than 60 s in the future and no older than 24 hours; a repeated jti returns {"status": "duplicate", "jti": "..."} without re-firing anything; each event's subject is keyed-hashed with HMAC-SHA-256 with its format bound in, the raw token is stored encrypted for forensics, and an action tag is recorded. An event that maps to a hard signal runs the four-branch cascade synchronously and is audited with the inbound jti. The acknowledgment is returned after processing, never before:
{
"status": "accepted",
"jti": "e9bc...",
"events_processed": [
{ "event_type": "session-revoked", "status": "processed", "event_id": "...",
"cascade": { "signal_class": "hard", "signal_type": "account_takeover", "dispatched": true,
"correlation_id": "pb-...", "delegates_revoked": 2, "negotiations_revoked": 0,
"shadows_frozen": 0, "tunnels_torn_down": 1, "cascade_latency_ms": 12 } }
]
}The JWKS is fetched over HTTPS only, with redirects refused, a size cap, a 5-second timeout and a short cache that refetches on an unknown kid so upstream key rotation is picked up without operator action.
Refusals you may see while testing the envelope:
| Status | Body | Cause |
|---|---|---|
| 400 | Missing tenant_id | No organization id in the body or the header |
| 404 | Tenant not found | The organization id does not exist |
| 401 | A signed SET (compact JWS) is required; unsigned SET receipt is not accepted. | set_jwt was absent. An unsigned JSON event is never processed |
| 400 | SET is not a valid compact JWS or is missing the 'iss' claim. | The token is not three segments, or has no issuer |
| 403 | No verified receiver stream is configured for this issuer. | No receiver stream for that iss with status: "verified" and a jwks_url. Promote it as in Step 2 |
| 429 | Too many requests. | More than 60 inbound tokens a minute from one source |
Step 4 · Check the stream state
Three reads tell you where a stream stands.
The stream row. {"action": "list_streams", "tenant_id": "YOUR_ORGANIZATION_ID"}. Check issuer and jwks_url are exactly what the upstream publishes, and read status. Note that events_delivered is a list of event type URIs copied from events_requested, not a delivery counter, so it does not move when tokens arrive.
The events. {"action": "list_events", "tenant_id": "YOUR_ORGANIZATION_ID", "limit": 50} returns inbound and outbound events newest first, each with its jti, event_type, subject_format, subject_identifier_hash, issuer, direction, status, actions_taken and timestamps, with optional event_type and status filters. A successful inbound delivery appears here with its jti; match it against the jti the transmitter logged. If it stays empty after the transmitter reports a send, re-read the stream's status, issuer and jwks_url first.
The verification handshake. {"action": "verify_stream", "tenant_id": "...", "stream_id": "..."} returns:
{ "status": "verification_initiated", "state": "9f1c...", "stream_uri": "https://idp.example.com/..." }It generates a state value, stamps last_verified_at on the row and writes an audit entry. It does not call the upstream verification endpoint and does not change status, so treat it as a local record that you attempted verification.
Audit. Every stream create, update, delete and verification writes a shield_audit_log entry under sse.stream.*, and a cascade driven by an inbound event writes cascade_execute with the inbound jti in its metadata. The audit log is the durable trail here.
Event-to-cascade mapping reference
Every inbound event produces a per-event action tag (always emitted, recorded in shield_sse_events.actions_taken). Hard signals additionally route through mapCaepToCascade and trigger executeCascade synchronously.
| Inbound CAEP Event | Per-event action tag | Cascade routing (when hit) |
|---|---|---|
session-revoked | session_invalidation_queued | hard · account_takeover |
credential-change (revoke, rotate, compromise, delete) | credential_review_flagged | hard · credential_leak |
credential-change (other change types) | credential_review_flagged | soft · behavioral_anomaly |
device-compliance-change (not-compliant) | device_posture_review | hard · device_compromise |
device-compliance-change (other) | device_posture_review | soft · behavioral_anomaly |
assurance-level-change | assurance_level_updated | soft · behavioral_anomaly |
token-claims-change | claims_refresh_queued | soft · behavioral_anomaly |
Soft signals are recorded for posture and analytics; only hard signals cause cascade revocation (delegates, A2A negotiations, shadow identities, BLAST tunnels). Unknown CAEP event types are logged and acknowledged but not dispatched, so vendor-specific extensions don't error out the stream. Continue to the SSF/CAEP Shared Signals reference guide for the full ingestion pipeline.
Related from the blog
- Running CAEP in Production: What Signing Outbound SETs Taught Us About Receiverssecurity · 13 min read
- SSE and CAEP: Closing the Cross-IdP Revocation Latency Gapsecurity · 21 min read
- Just-in-Time Capability Tokens for Agents: Why Persistent OAuth Scopes Are the New Standing Privilegesecurity · 14 min read