> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# onSessionInvalidated

> Registers a listener that runs once when the backend rejects this Portal instance's session, so your app can route to sign-in.

**Function Signature**

```swift theme={null}
@discardableResult
public func onSessionInvalidated(
    _ listener: @escaping @MainActor () -> Void
) -> PortalSessionInvalidationHandle
```

Runs `listener` after the backend rejects this `Portal`'s credential (an HTTP `401` on a Portal-owned request, or an MPC `AUTH_FAILED`) and the SDK has invalidated it.

**Parameters**

* **`listener`**: Called at most once, on the main actor. This is your cue to clear local state and show the sign-in screen. It is retained until it runs or the credential is deallocated, so capture `self` weakly.

**Returns**

* **`PortalSessionInvalidationHandle`**: Call `cancel()` to remove the listener. `cancel()` also suppresses a delivery that is already queued but has not run, and it is safe to call more than once or from inside the listener. The handle is **not** cancelled on deallocation, so you only need to keep it if you intend to unsubscribe. A `Portal` built from a Client API Key returns the shared `PortalSessionInvalidationHandle.spent`, and the listener never runs.

**Notes**

* **A late subscriber does not miss the event.** `Portal.init` starts an authenticated request immediately. If the backend rejects it before you subscribe, the rejection is replayed: the listener still runs once, on the main actor.
* Do not subscribe again on the same `Portal` from inside the listener. The instance is spent, and a new subscription would be called as well.
* Fires at most once per credential, and never for a host-initiated `clearSession()`.
* The request that triggered the rejection still fails with its own error, and the listener runs in addition. Every later call on the `Portal` throws `PortalCredentialError.sessionInvalidated`, whose `requiresReauthentication` is `true`.
* Only requests that carried the session to a Portal-owned host count: Portal's own domains, or a host you configured on `Portal`, `PortalConnect` or `PortalAuth`. A `401` from a third-party RPC gateway or Google Drive, or a failed or cancelled passkey, does not invalidate the session.
* The session is already invalidated when the listener runs, and its stored copy deleted. Do local cleanup, call `auth.clearPersistedSession()` so a re-delivered redirect cannot replay it, and do not call `clearSession()` again.
* Available starting from SDK version 8.0.0.

**Example Usage**

```swift theme={null}
import PortalSwift

func adopt(session: PortalSession) throws {
    let portal = try Portal(credentials: session)

    let handle = portal.onSessionInvalidated { [weak self] in
        guard let self else { return }
        Task {
            do {
                try await self.auth.clearPersistedSession()
            } catch {
                print("Error clearing the persisted session: \(error)")
            }
            self.portal = nil
            self.showSignIn()
        }
    }

    self.portal = portal
    self.sessionInvalidationHandle = handle   // call handle.cancel() to unsubscribe
}
```

**Related Documentation**

* [clearSession reference](./clearsession)
* [restoreSession reference](./portalauthrestoresession)
* [Handle session invalidation](../guide/client-auth#handle-session-invalidation)
* [End the session](../guide/client-auth#end-the-session)
