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.

Last reviewed September 16, 2026Fresh

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_streams and list_events requires it; those two require membership. There is no API-key lane on this function and no sse scope.
  • Your organization id, from Developer > Settings, field Organization ID.
  • An upstream transmitter that publishes a /.well-known/ssf-configuration document, 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-configuration

Note 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:

StatusBodyCause
400Missing tenant_idNo organization id in the body or the header
404Tenant not foundThe organization id does not exist
401A signed SET (compact JWS) is required; unsigned SET receipt is not accepted.set_jwt was absent. An unsigned JSON event is never processed
400SET is not a valid compact JWS or is missing the 'iss' claim.The token is not three segments, or has no issuer
403No 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
429Too 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 EventPer-event action tagCascade routing (when hit)
session-revokedsession_invalidation_queuedhard · account_takeover
credential-change (revoke, rotate, compromise, delete)credential_review_flaggedhard · credential_leak
credential-change (other change types)credential_review_flaggedsoft · behavioral_anomaly
device-compliance-change (not-compliant)device_posture_reviewhard · device_compromise
device-compliance-change (other)device_posture_reviewsoft · behavioral_anomaly
assurance-level-changeassurance_level_updatedsoft · behavioral_anomaly
token-claims-changeclaims_refresh_queuedsoft · 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