> This page is for For Developers.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://whop.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://whop.ferndocs.com/_mcp/server.

# Authentication

> Verify the current user and check their access level inside your Whop app.

Whop apps render inside an iframe on `whop.com`. To figure out *who* is making a request to your app, Whop passes a short-lived JWT in the `x-whop-user-token` header on every same-origin request. You verify this token with the SDK to get a user ID, then check what that user is allowed to see.

## Verify the iframe user token

Call `verifyUserToken` on each request. It reads the `x-whop-user-token` header, validates the JWT signature and expiry, and returns the user ID. Invalid tokens throw by default. Each SDK also has a non-throwing variant:

| Language   | Non-throwing variant                              |
| ---------- | ------------------------------------------------- |
| TypeScript | Pass `{ dontThrow: true }` as the second argument |
| Python     | Pass `dont_throw=True` as a keyword argument      |
| Ruby       | Use `verify_user_token` (without the `!`)         |

**`JS/TS + NextJS`**

```typescript JS/TS + NextJS
import { headers } from "next/headers";
import { whopsdk } from "../../../lib/whop-sdk";

export default async function MyServerRenderedPage() {
	const { userId } = await whopsdk.verifyUserToken(await headers());

    // ... the rest of your component / api route etc...
}
```

**`JS/TS + ExpressJS`**

```javascript JS/TS + ExpressJS
import express from "express";
import { whopsdk } from "./lib/whop-sdk";

const app = express();

app.get("/my-route", async (req, res) => {
    const { userId } = await whopsdk.verifyUserToken(req.headers);

    // ... the rest of your route handler
});
```

**`Python + FastAPI`**

```python Python + FastAPI
from fastapi import FastAPI, Request
from lib.whop_sdk import whopsdk

app = FastAPI()

@app.get("/my-route")
async def my_route(request: Request):
    result = await whopsdk.verify_user_token(request.headers)
    user_id = result.user_id

    # ... the rest of your route handler
```

**`Ruby on Rails`**

```ruby Ruby on Rails
class MyController < ApplicationController
  def my_action
    auth_result = whopsdk.verify_user_token!(request.headers)
    user_id = auth_result.user_id

    # ... the rest of your controller action
  end
end
```

The token is only attached to requests hitting the `window.location.origin` of your iframe. Examples:

* Relative page navigations: `<a href="/sub_page">...</a>`
* Fetches without a domain: `await fetch("/api/quizzes")`

All of these resolve to your `App.base_url` domain.

#### My API is on a different domain

If your frontend runs on `example.com` but your API runs on `api.example.com`, reverse-proxy API requests through `example.com/api` so they carry the `x-whop-user-token` header.

* **Cloudflare**: create an ["origin rule"](https://developers.cloudflare.com/rules/origin-rules/features/#dns-record) from `/api` on `example.com` to rewrite to `api.example.com/api`.
* **Next.js**: add a [rewrite](https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites) in `next.config.mjs`.
* **nginx / Caddy**: use the rewrite primitive in your server config.

> **Note**
>
> This setup is required because of strict browser cross-origin cookie policies.

## Local setup

Whop ships a local reverse proxy that matches the production iframe + cookie behavior, so the code you write works identically on `localhost` and in production. See the [dev proxy guide](/developer/guides/dev-proxy) for setup.

## Check access

Now that you know who's making the request, check what they're allowed to see. Use the [`checkAccess`](/api-reference/users/check-access) method to verify access to an Experience, Company, or Product.

**`JS/TS`**

```typescript JS/TS
import { whopsdk } from "../../../lib/whop-sdk";

const response = await whopsdk.users.checkAccess(
    "resource_id",
    { id: "user_xxxxxxxxxxxxx" }
);

// Response:
// {
//   "has_access": true,
//   "access_level": "customer"
// }
```

**`Python`**

```python Python
response = whopsdk.users.check_access(
    resource_id="resource_id",
    id="user_xxxxxxxxxxxxx",
)

# Response:
# {
#   "has_access": True,
#   "access_level": "customer"
# }
```

**`Ruby`**

```ruby Ruby
response = whopsdk.users.check_access(
    "resource_id",
    id: "user_xxxxxxxxxxxxx"
)

# Response:
# {
#   "has_access": true,
#   "access_level": "customer"
# }
```

**`cURL`**

```bash cURL
curl --request GET \
  --url https://api.whop.com/api/v1/users/{id}/access/{resource_id} \
  --header 'Authorization: Bearer <token>'
```

### Resource IDs and access levels

| `resource_id` prefix | Checks access to |
| -------------------- | ---------------- |
| `biz_xxxx`           | Company          |
| `prod_xxxx`          | Product          |
| `exp_xxxx`           | Experience       |

The response's `access_level` is one of:

| Level       | Meaning                                                                                                                                                                              |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `customer`  | User has a valid membership. For an experience, they have a membership to any product connected to it. For a product, to that specific product. For a company, to any product on it. |
| `admin`     | User is a team member of the company (any role, including moderator).                                                                                                                |
| `no_access` | User has no access (`has_access` is `false`).                                                                                                                                        |

### Customer app (Experience View)

Customer apps should gate on the `experienceId` passed in path params. Return `no_access` or redirect if the viewer doesn't have a valid membership.

```typescript
import { headers } from "next/headers";
import { whopsdk } from "../../../lib/whop-sdk";

export default async function ExperiencePage({
    params,
}: {
    params: Promise<{ experienceId: string }>;
}) {
    const { experienceId } = await params;
    const { userId } = await whopsdk.verifyUserToken(await headers());

    const access = await whopsdk.users.checkAccess(
        experienceId,
        { id: userId }
    );

    if (!access.has_access) {
        return <div>Access denied</div>;
    }

    return <div>Welcome to the experience!</div>;
}
```

The same pattern works in an API route. Swap `headers()` for `request.headers` and return a `403 Response` instead of JSX.

### Dashboard app (Dashboard View)

Dashboard apps should gate on `access_level === "admin"` so only company team members can load the view.

```typescript
import { headers } from "next/headers";
import { whopsdk } from "../../../lib/whop-sdk";

export default async function DashboardPage({
    params,
}: {
    params: Promise<{ companyId: string }>;
}) {
    const { companyId } = await params;
    const { userId } = await whopsdk.verifyUserToken(await headers());

    const access = await whopsdk.users.checkAccess(
        companyId,
        { id: userId }
    );

    if (access.access_level !== "admin") {
        return <div>Admin access required</div>;
    }

    return <div>Welcome to the dashboard!</div>;
}
```

Same pattern in an API route. Swap `headers()` for `request.headers` and return a `403 Response` instead of JSX.

## Next steps

#### [Run a local dev proxy](/developer/guides/dev-proxy)

Match the production iframe + cookie setup on localhost.

#### [Request permissions](/developer/guides/permissions)

Add the scopes your app needs before publishing.

#### [Listen to webhooks](/developer/guides/webhooks)

Receive payment, membership, and entry events on your server.

#### [Build an app view](/developer/guides/app-views)

Set up dashboard views, experiences, and discover listings.