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. A receiver stream also requires audience, the aud value the upstream puts in its tokens, and the call answers 400 without it. delivery_method defaults to push. issuer and jwks_url default to null, and a receiver stream needs both to be usable at all. 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.
Verify the stream. Once issuer, jwks_url and audience match what the upstream publishes, run the verification exchange in Step 4: the upstream echoes a state in a verification event and the stream moves to verified. If the upstream cannot send one, an admin can 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. Setting it by hand is an assertion by an admin that this issuer is one you trust; nothing contacts the upstream on your behalf. 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 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; an event already recorded for that issuer, jti and event type is not run again, and a token whose events were all recorded returns {"status": "duplicate", "jti": "..."}; 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, 202, is returned after processing, never before; if a cascade fails the answer is 500 with status: "partial", so the upstream redelivers and only the failed events run again:
{
"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, from a host that resolves to public addresses, 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. Verify it as in Step 4 |
| 403 | The receiver stream for this issuer has no audience configured; set one and address the SET to it. | The stream has no audience |
| 403 | The SET's audience matches no receiver stream configured for this issuer. | The token's aud is not the stream's audience |
| 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...", "instructions": "Have the transmitter send a verification event ...", "stream_uri": "https://idp.example.com/..." }It generates a state value, keeps only its digest on the row and writes an sse.stream.verify_requested audit entry. It does not call the upstream: the upstream sends a verification event (https://schemas.openid.net/secevent/ssf/event-type/verification) carrying the state, signed and addressed like any other token, and the stream then moves to verified, stamps last_verified_at and writes sse.stream.verified. A verification event with any other state answers 400.
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 · account_takeover |
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.