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

# Session Management

> Manage client sessions in server-side environments with Para Server SDK

export const Link = ({href, label, newTab = false}) => {
  const [isHovered, setIsHovered] = useState(false);
  return <a href={href} target={newTab ? '_blank' : '_self'} rel={newTab ? 'noopener noreferrer' : undefined} className="not-prose inline-block relative text-black font-semibold cursor-pointer border-b-0 no-underline" onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      {label}
      <span className={`absolute left-0 bottom-0 w-full rounded-sm bg-gradient-to-r from-orange-600 to-purple-600 transition-all duration-300 ${isHovered ? 'h-0.5' : 'h-px'}`} />
    </a>;
};

export const Card = ({imgUrl, title, description, href, horizontal = false, newTab = false}) => {
  const [isHovered, setIsHovered] = useState(false);
  const handleClick = e => {
    e.preventDefault();
    if (newTab) {
      window.open(href, '_blank', 'noopener,noreferrer');
    } else {
      window.location.href = href;
    }
  };
  return <div className={`not-prose relative my-2 p-[1px] rounded-xl transition-all duration-300 ${isHovered ? 'bg-gradient-to-r from-[#FF4E00] to-[#874AE3]' : 'bg-gray-200'}`} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      <a href={href} onClick={handleClick} className={`not-prose flex ${horizontal ? 'flex-row' : 'flex-col'} font-normal h-full bg-white overflow-hidden w-full cursor-pointer rounded-[11px] no-underline`}>
        {imgUrl && <div className={`relative overflow-hidden flex-shrink-0 ${horizontal ? 'w-[30%] rounded-l-[11px]' : 'w-full'}`} onClick={e => e.stopPropagation()}>
            <img src={imgUrl} alt={title} className="w-full h-full object-cover pointer-events-none select-none" draggable="false" />
            <div className="absolute inset-0 pointer-events-none" />
          </div>}
        <div className={`flex-grow px-6 py-5 ${horizontal ? 'w-[70%]' : 'w-full'} flex flex-col ${horizontal && imgUrl ? 'justify-center' : 'justify-start'}`}>
          {title && <h2 className="font-semibold text-base text-gray-800 m-0">{title}</h2>}
          {description && <div className={`font-normal text-gray-500 re leading-6 ${horizontal || !imgUrl ? 'mt-0' : 'mt-1'}`}>
              <p className="m-0 text-xs">{description}</p>
            </div>}
        </div>
      </a>
    </div>;
};

Para's Server SDK can import a session that a user created in your client application. This lets your backend perform authenticated operations for that user, including signing, without asking the user to re-authenticate for the server request. Session validity and duration are enforced by the Para API and the API key's session length configuration.

## Importing Client Sessions

Export the authenticated client session, send it to your backend over HTTPS, then import it into a fresh `ParaServer` instance for the request.

<Warning>
  Create a new `ParaServer` instance for each imported session. A server SDK instance holds one active session at a time, so reusing one instance across users can mix request state.
</Warning>

### Client-Side Session Export

Use `waitAndExportSession()` after the user authenticates:

```typescript theme={null}
const serializedSession = await para.waitAndExportSession();

await fetch("/api/sign-message", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    session: serializedSession,
    message: "Hello from Para",
  }),
});
```

<Info>
  `waitAndExportSession()` waits until the SDK has reached an authenticated state before reading session data. Use it for handoff flows immediately after login.
</Info>

<Warning>
  If your backend needs to sign for the user, do not export with `{ excludeSigners: true }`. That option removes the wallet signer data required for server-side signing.
</Warning>

If your backend only needs to validate the user session and will not sign, you can export without signer data:

```typescript theme={null}
const sessionWithoutSigners = await para.waitAndExportSession({
  excludeSigners: true,
});
```

### Server-Side Session Import

Import the serialized session before running user-authenticated operations:

```typescript theme={null}
import { Para as ParaServer } from "@getpara/server-sdk";
import express from "express";

const app = express();
app.use(express.json());

const paraApiKey = process.env.PARA_API_KEY;
if (!paraApiKey) {
  throw new Error("PARA_API_KEY is required");
}

app.post("/api/sign-message", async (req, res) => {
  const { session, message } = req.body;
  const para = new ParaServer(paraApiKey);

  await para.importSession(session);

  if (!(await para.isSessionActive())) {
    return res.status(401).json({ error: "Session expired" });
  }

  const sessionExtended = await para.keepSessionAlive();
  if (!sessionExtended) {
    return res.status(401).json({ error: "Session expired" });
  }

  const walletId = para.findWalletId();
  const result = await para.signMessage({
    walletId,
    messageBase64: Buffer.from(message).toString("base64"),
  });

  return res.status(200).json({
    walletId,
    signature: result.signature,
  });
});
```

<Note>
  `keepSessionAlive()` extends the active imported session according to the API key's configured session length. It returns `false` if the session cannot be extended.
</Note>

### Handling Multiple User Sessions

Each exported session belongs to one authenticated user. Import each session into its own `ParaServer` instance when your backend handles multiple users or parallel requests:

```typescript theme={null}
import { Para as ParaServer } from "@getpara/server-sdk";

async function signForImportedSession(session: string, message: string) {
  const para = new ParaServer(process.env.PARA_API_KEY!);
  await para.importSession(session);

  const walletId = para.findWalletId();
  return para.signMessage({
    walletId,
    messageBase64: Buffer.from(message).toString("base64"),
  });
}

const [firstSignature, secondSignature] = await Promise.all([
  signForImportedSession(firstUserSession, "Message for first user"),
  signForImportedSession(secondUserSession, "Message for second user"),
]);
```

## Session Validation

You can validate sessions on the server side to ensure they're still active before performing operations.

### Using the Para Client

```typescript theme={null}
import { Para as ParaServer } from "@getpara/server-sdk";

app.post("/api/authenticated-action", async (req, res) => {
  const paraServer = new ParaServer("YOUR_API_KEY");

  await paraServer.importSession(req.body.session);

  if (!(await paraServer.isSessionActive())) {
    return res.status(401).json({ error: "Session expired" });
  }

  return res.status(200).json({ success: true });
});
```

### Using JWT Authentication

Once a user is signed in, you can request a Para JWT token. This token will provide attestations for the user's ID, their identity, and any wallets they have provisioned via your application.

To request a token, use the `issueJwt` method. The method returns the token itself as well as the JWKS key ID (`kid`) for the keypair that signed it.

```typescript theme={null}
const paraServer  = new ParaServer('your-api-key');

const { token, keyId } = await paraServer.issueJwt();
```

<Info>The token's expiry will be determined by your customized session length, or else will default to 30 minutes. Issuing a token, like most authenticated API operations, will also renew and extend the session for that duration.</Info>

<Tip>The token's `aud` field will be set to your API key's unique ID, linking it specifically to your application.</Tip>

Depending on the user in question, a decoded token payload might resemble the following:

<CodeGroup>
  ```json Email theme={null}
  {
    "data": {
      "userId": "d5358219-38d3-4650-91a8-e338131d1c5e",
      "wallets": [
        {
          "id": "de4034f1-6b0f-4a98-87a5-e459db4d3a03",
          "type": "EVM",
          "address": "0x9dd3824f045c77bc369485e8f1dd6b452b6be617",
          "publicKey": "0x0465434f76c8321f386856c44e735fd365a09d42c1da03489184b651c2052ea1c7b19c54722ed828458c1d271cc590b0818d8c7df423f71e92683f9e819095a8c6"
        },
        {
          "id": "d70f64e4-266a-457e-9cea-eeb42341a975",
          "type": "SOLANA",
          "address": "EEp7DbBu5yvgf7Pr9W17cATPjCqUxY8K8R3dFbg53a3W",
          "publicKey": ""
        }
      ],
      "email": "email@example.com",
      "authType": "email",
      "identifier": "email@example.com",
      "oAuthMethod": "google" // or: undefined | "x" | "discord" | "facebook" | "apple"
    },
    "iat": 1745877709,
    "exp": 1745879509,
    "aud": "a31b8f2e-7c6d-4e5a-9f8b-1d2c3a4b5e6f",
    "sub": "d5358219-38d3-4650-91a8-e338131d1c5e"
  }
  ```

  ```json Phone theme={null}
  {
    "data": {
      "userId": "d5358219-38d3-4650-91a8-e338131d1c5e",
      "wallets": [
        {
          "id": "de4034f1-6b0f-4a98-87a5-e459db4d3a03",
          "type": "EVM",
          "address": "0x9dd3824f045c77bc369485e8f1dd6b452b6be617",
          "publicKey": "0x0465434f76c8321f386856c44e735fd365a09d42c1da03489184b651c2052ea1c7b19c54722ed828458c1d271cc590b0818d8c7df423f71e92683f9e819095a8c6"
        },
        {
          "id": "d70f64e4-266a-457e-9cea-eeb42341a975",
          "type": "SOLANA",
          "address": "EEp7DbBu5yvgf7Pr9W17cATPjCqUxY8K8R3dFbg53a3W",
          "publicKey": ""
        }
      ],
      "phone": "+13105551234",
      "authType": "phone",
      "identifier": "+13105551234"
    },
    "iat": 1745877709,
    "exp": 1745879509,
    "aud": "a31b8f2e-7c6d-4e5a-9f8b-1d2c3a4b5e6f",
    "sub": "d5358219-38d3-4650-91a8-e338131d1c5e"
  }
  ```

  ```json Telegram theme={null}
  {
    "data": {
      "userId": "d5358219-38d3-4650-91a8-e338131d1c5e",
      "wallets": [
        {
          "id": "de4034f1-6b0f-4a98-87a5-e459db4d3a03",
          "type": "EVM",
          "address": "0x9dd3824f045c77bc369485e8f1dd6b452b6be617",
          "publicKey": "0x0465434f76c8321f386856c44e735fd365a09d42c1da03489184b651c2052ea1c7b19c54722ed828458c1d271cc590b0818d8c7df423f71e92683f9e819095a8c6"
        },
        {
          "id": "d70f64e4-266a-457e-9cea-eeb42341a975",
          "type": "SOLANA",
          "address": "EEp7DbBu5yvgf7Pr9W17cATPjCqUxY8K8R3dFbg53a3W",
          "publicKey": ""
        }
      ],
      "telegramUserId": "1234567890",
      "authType": "telegram",
      "identifier": "1234567890"
    },
    "iat": 1745877709,
    "exp": 1745879509,
    "aud": "a31b8f2e-7c6d-4e5a-9f8b-1d2c3a4b5e6f",
    "sub": "d5358219-38d3-4650-91a8-e338131d1c5e"
  }
  ```

  ```json Farcaster theme={null}
  {
    "data": {
      "userId": "d5358219-38d3-4650-91a8-e338131d1c5e",
      "wallets": [
        {
          "id": "de4034f1-6b0f-4a98-87a5-e459db4d3a03",
          "type": "EVM",
          "address": "0x9dd3824f045c77bc369485e8f1dd6b452b6be617",
          "publicKey": "0x0465434f76c8321f386856c44e735fd365a09d42c1da03489184b651c2052ea1c7b19c54722ed828458c1d271cc590b0818d8c7df423f71e92683f9e819095a8c6"
        },
        {
          "id": "d70f64e4-266a-457e-9cea-eeb42341a975",
          "type": "SOLANA",
          "address": "EEp7DbBu5yvgf7Pr9W17cATPjCqUxY8K8R3dFbg53a3W",
          "publicKey": ""
        }
      ],
      "farcasterUsername": "FarcasterUsername",
      "authType": "farcaster",
      "identifier": "FarcasterUsername"
    },
    "iat": 1745877709,
    "exp": 1745879509,
    "aud": "a31b8f2e-7c6d-4e5a-9f8b-1d2c3a4b5e6f",
    "sub": "d5358219-38d3-4650-91a8-e338131d1c5e"
  }
  ```

  ```json External Wallet theme={null}
  {
    "data": {
      "userId": "d5358219-38d3-4650-91a8-e338131d1c5e",
      "wallets": [
        {
          "id": "de4034f1-6b0f-4a98-87a5-e459db4d3a03",
          "type": "EVM",
          "address": "0x9dd3824f045c77bc369485e8f1dd6b452b6be617",
          "publicKey": "0x0465434f76c8321f386856c44e735fd365a09d42c1da03489184b651c2052ea1c7b19c54722ed828458c1d271cc590b0818d8c7df423f71e92683f9e819095a8c6"
        },
        {
          "id": "d70f64e4-266a-457e-9cea-eeb42341a975",
          "type": "SOLANA",
          "address": "EEp7DbBu5yvgf7Pr9W17cATPjCqUxY8K8R3dFbg53a3W",
          "publicKey": ""
        }
      ],
      "externalWalletAddress": "0xaD6b78193b78e23F9aBBB675734f4a2B3559598D",
      "authType": "externalWallet",
      "identifier": "0xaD6b78193b78e23F9aBBB675734f4a2B3559598D",
      "externalWallet": {
        "address": "0xaD6b78193b78e23F9aBBB675734f4a2B3559598D",
        "type": "EVM",
        "provider": "MetaMask"
      }
    },
    "iat": 1745877709,
    "exp": 1745879509,
    "aud": "a31b8f2e-7c6d-4e5a-9f8b-1d2c3a4b5e6f",
    "sub": "d5358219-38d3-4650-91a8-e338131d1c5e"
  }
  ```
</CodeGroup>

Para's JSON Web Keys Set (JWKS) file(s) are available at the following URLs:

| Environment | JWKS URL                                             |
| ----------- | ---------------------------------------------------- |
| BETA        | `https://api.beta.getpara.com/.well-known/jwks.json` |
| PROD        | `https://api.getpara.com/.well-known/jwks.json`      |

### Using Verification Tokens

For non-Node.js servers or scenarios where you only need to validate a session without importing it, Para provides dedicated verification endpoints:

```typescript theme={null}
// Client-side: Get a verification token
const verificationToken = await para.getVerificationToken();

// Send to your server
await fetch("/api/verify-session", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ verificationToken }),
});
```

On your server, verify the token against Para's API.

<Info>
  Use your Secret API Key from the Developer Portal to authenticate requests to the verification endpoints. This key is different from the public-facing API Key used in the Para client.
</Info>

<CodeGroup>
  ```typescript Node.js theme={null}
  // Server-side verification
  app.post("/api/verify-session", async (req, res) => {
    const { verificationToken } = req.body;

    if (!verificationToken) {
      return res.status(400).json({ error: "Missing verification token" });
    }

    // Set the correct URL based on your environment
    const verifyUrl = "https://api.beta.getpara.com/sessions/verify";
    
    const response = await fetch(verifyUrl, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-external-api-key": "YOUR_SECRET_API_KEY"
      },
      body: JSON.stringify({ verificationToken }),
    });

    if (response.status === 403) {
      return res.status(403).json({ error: "Session expired" });
    }

    const userData = await response.json();
    return res.status(200).json({ userData });
  });
  ```

  ```python Python theme={null}
  from flask import Flask, request, jsonify
  import requests

  app = Flask(__name__)

  @app.route('/api/verify-session', methods=['POST'])
  def verify_session():
      data = request.get_json()
      verification_token = data.get("verificationToken")
      
      if not verification_token:
          return jsonify({"error": "Missing verification token"}), 400
      
      # Set the correct URL based on your environment
      verify_url = "https://api.beta.getpara.com/sessions/verify"
      
      response = requests.post(
          url=verify_url,
          json={"verificationToken": verification_token},
          headers={
              "content-type": "application/json",
              "x-external-api-key": "YOUR_SECRET_API_KEY"
          }
      )
      
      if response.status_code == 403:
          return jsonify({"error": "Session expired"}), 403
      
      user_data = response.json()
      # user_data contains { authType, identifier }
      
      # Proceed with authenticated operations
      return jsonify({"userData": user_data})

  if __name__ == '__main__':
      app.run(debug=True)
  ```
</CodeGroup>

The verification endpoints are environment-specific:

| Environment | Verification URL                               |
| ----------- | ---------------------------------------------- |
| BETA        | `https://api.beta.getpara.com/sessions/verify` |
| PROD        | `https://api.getpara.com/sessions/verify`      |

The verification response will contain the authentication type, identifier, and optionally the OAuth method used:

```typescript theme={null}
{
  authType: "email" | "phone" | "farcaster" | "telegram" | "externalWallet";
  identifier: string;
  oAuthMethod?: "google" | "x" | "discord" | "facebook" | "apple";
}
```

## Session Management

### Maintaining Session Validity

To extend the validity of the session imported into the current `ParaServer` instance, call `keepSessionAlive()`:

```typescript theme={null}
const success = await paraServer.keepSessionAlive();

if (!success) {
  throw new Error("Session expired");
}
```

<Note>
  Session length is configured per API key in the <Link label="Para Developer Portal" href="https://developer.getpara.com" /> or CLI. The Para API enforces that duration, and `keepSessionAlive()` extends the active session according to the configured value.
</Note>

<Info>
  `refreshSession()` starts a login or refresh flow and returns a login URL. For imported server sessions, use `keepSessionAlive()` to extend the active session.
</Info>

### Best Practices

1. **Create a fresh server instance per session**: Initialize a new Para Server SDK instance for each imported user session or request.

2. **Secure session transport**: Always use HTTPS when transferring sessions between client and server. Do not log serialized sessions.

3. **Export signer data only when needed**: Use `{ excludeSigners: true }` only when the server does not need to sign.

4. **Validate before operations**: Check that the imported session is active before performing authenticated operations.

5. **Handle expiration explicitly**: If the session is expired or cannot be extended, ask the client to authenticate again and export a new session.

6. **Use verification tokens for auth-only checks**: When you only need to verify who the user is, use verification tokens instead of importing a full session.

7. **Configure session length intentionally**: Set the API key's session length in the Developer Portal or CLI based on your application's security model.

## Verifying Wallet Ownership

To verify that a wallet address matches one of your users' embedded wallets, you can send a request to one of the following endpoints:

| Environment | URL                                           |
| ----------- | --------------------------------------------- |
| BETA        | `https://api.beta.getpara.com/wallets/verify` |
| PROD        | `https://api.getpara.com/wallets/verify`      |

<Info>
  Use your Secret API Key from the Developer Portal to authenticate requests to this endpoint. This key is different from the public-facing API Key used in the Para client.
</Info>

Pass the address for the wallet in the POST request body:

```typescript Node.js theme={null}
// Server-side verification
app.post("/api/verify-wallet", async (req, res) => {
  const { address } = req.body;
  
  if (!address) {
    return res.status(400).json({ error: "Missing address" });
  }
  
  // Set the correct URL based on your environment
  const verifyUrl = "https://api.beta.getpara.com/wallets/verify";
  
  const response = await fetch(verifyUrl, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-external-api-key": "YOUR_SECRET_API_KEY"
    },
    body: JSON.stringify({ address }),
  });

  if (response.status === 404) {
    return res.status(404).json({ error: `Wallet not found with address: ${address}` });
  }

  const { walletId } = await response.json();
  return res.status(200).json({ walletId });
});
```

## Learn More

For more information about client-side session management and authentication, refer to our web documentation:

<Card title="Web Session Management" imgUrl="/images/v2/general-sessions.png" href="/v2/react/guides/sessions" description="Learn about client-side session management, authentication, and session exports" horizontal />

## Examples

To learn more about using sessions on the server, check out this example. Each example route will have both pregen and session based routes for you to test with.

<Card title="Session Import Example" imgUrl="/images/v2/general-examples.png" href="https://github.com/getpara/examples-hub/tree/2.0.0/server/with-node/" description="Complete example of importing and using client sessions on a Node.js server" horizontal />
