Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.whop.com/llms.txt

Use this file to discover all available pages before exploring further.

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:
LanguageNon-throwing variant
TypeScriptPass { dontThrow: true } as the second argument
PythonPass dont_throw=True as a keyword argument
RubyUse verify_user_token (without the !)
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...
}
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.
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” from /api on example.com to rewrite to api.example.com/api.
  • Next.js: add a rewrite in next.config.mjs.
  • nginx / Caddy: use the rewrite primitive in your server config.
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 for setup.

Check access

Now that you know who’s making the request, check what they’re allowed to see. Use the checkAccess method to verify access to an Experience, Company, or Product.
import { whopsdk } from "@/lib/whop-sdk";

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

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

Resource IDs and access levels

resource_id prefixChecks access to
biz_xxxxCompany
prod_xxxxProduct
exp_xxxxExperience
The response’s access_level is one of:
LevelMeaning
customerUser 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.
adminUser is a team member of the company (any role, including moderator).
no_accessUser 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.
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.
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

Match the production iframe + cookie setup on localhost.

Request permissions

Add the scopes your app needs before publishing.

Listen to webhooks

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

Build an app view

Set up dashboard views, experiences, and discover listings.