# Add apps Source: https://docs.whop.com/add-apps Learn how to add apps to your whop Apps are the building blocks for your business on Whop.  Here’s how it works: You create your own whop, then pick and choose which apps to include. Want a chat room for your community? Add the **Chat** app. Selling a course? Add the **Course** app.  There are hundreds of apps to choose from, and you can add any of them from the Whop App Store. ## How to add apps to your whop You can find all the available apps in the Whop App Store. * Inside your whop, click **Add app** * Browse the different categories of apps to find the apps your members will find most valuable Learn more about what apps you should add to your whop [here](/apps/docs/whop-apps/what-are-whop-apps). When you find an app you'd like to add, click **Add**. You will see the app has been added in the left-hand column of your whop. You can add as many apps as you want, and add each app as many times as you'd like (for example, you can add the Chat app multiple times to create different chat groups for different topics). ## Configure the app settings Each app comes with its own settings that let you control the experience and functionality. * **Chat app**: Choose who can post and react, ban links or images * **Courses app**: Upload videos, add files, create quizzes * **Events app**: Add upcoming events with details and links To customize any app: 1. Click on the app in your Whop 2. Click the **Configure** settings Only you (and other team members) can see and edit the configuration settings of each app. ## Learn more about Whop apps Learn more about what Whop apps are and how to use them. Learn more about the Whop App Store. ## Next steps To launch your internet business, make sure you’ve completed the rest of the setup steps: Choose your pricing model and what members get when they join. Your store page is where visitors come to learn about your offer. # List AccessPass Source: https://docs.whop.com/api-reference/access-passes/list-accesspass https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /access_passes Lists access passes for a company # Retrieve AccessPass Source: https://docs.whop.com/api-reference/access-passes/retrieve-accesspass https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /access_passes/{id} Retrieves an access pass by ID or route # Retrieve Company Source: https://docs.whop.com/api-reference/companies/retrieve-company https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /companies/{id} Retrieves an company by ID # List CourseLessonInteraction Source: https://docs.whop.com/api-reference/course-lesson-interactions/list-courselessoninteraction https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /course_lesson_interactions Lists course lesson interactions # Retrieve CourseLessonInteraction Source: https://docs.whop.com/api-reference/course-lesson-interactions/retrieve-courselessoninteraction https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /course_lesson_interactions/{id} Retrieves a course lesson interaction by ID # Create Invoice Source: https://docs.whop.com/api-reference/invoices/create-invoice https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml post /invoices Creates an invoice # List Invoice Source: https://docs.whop.com/api-reference/invoices/list-invoice https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /invoices Lists invoices # Retrieve Invoice Source: https://docs.whop.com/api-reference/invoices/retrieve-invoice https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml get /invoices/{id} Retrieves an invoice by ID or token # Void Invoice Source: https://docs.whop.com/api-reference/invoices/void-invoice https://app.stainless.com/api/spec/documented/whopsdk/openapi.documented.yml post /invoices/{id}/void Void an invoice # Dashboard View Source: https://docs.whop.com/apps/app-views/dashboard-view The dashboard view is a business-focused view that allows creators to access apps directly from their dashboard. This view is designed for apps that help businesses grow and manage operations. Dashboard View ## Configure 1. Go to the [developer dashboard](https://whop.com/dashboard/developer) 2. Create a new app or select an existing one 3. Scroll down to the **Hosting** section App Settings Enter your path for the dashboard view. The recommended default path is `/dashboard/[companyId]`. Dashboard View Path * `[companyId]` is used to provide the accessed company ID: `/dashboard/[companyId] -> /dashboard/biz_***` * `[restPath]` is used for deep linking to specific sections of your app: `/dashboard/[companyId]/[restPath] -> /dashboard/biz_***/posts/1` ## Preview Click the preview button next to the field, this will take you to your app's dashboard view. You will be prompted to install your app if you haven't already. Preview App Button If you've already installed your app, you can access it from your dashboard under the **apps** section. Dashboard Sidebar Apps Section 1. Open the dev tools by clicking the **cog** button 2. Set the environment to **localhost** ## Validate access You can check if a user is an authorized member of a company by using the SDK method [checkIfUserHasAccessToCompany](/sdk/api/access/check-if-user-has-access-to-company). ```ts const access = await whopSdk.access.checkIfUserHasAccessToCompany({ companyId: "biz_***", userId: "user_***", }); // No access // ^? { hasAccess: false, accessLevel: "no_access" } // Admin access // ^? { hasAccess: true, accessLevel: "admin" } ``` See [Validate company access](/sdk/validate-access#validate-company-access) for more information. ## Examples * [Next.js](https://github.com/whopio/whop-nextjs-app-template/blob/main/app/dashboard/%5BcompanyId%5D/page.tsx) * [React Native](https://github.com/whopio/whop-sdk-ts/blob/main/packages/create-react-native/template/src/views/dashboard-view.tsx) # Discover View Source: https://docs.whop.com/apps/app-views/discover-view The discover view is a public-facing view that helps potential users explore what your app offers. This view showcases your app's capabilities and highlights success stories from other communities. ## Configure 1. Go to the [developer dashboard](https://whop.com/dashboard/developer) 2. Create a new app or select an existing one 3. Scroll down to the **Hosting** section App Settings Enter your path for the discover view. The recommended default path is `/discover`. Discover View Path ## Preview Click the preview button or access the view at `https://whop.com/discover/app/`. Preview Discover View ## Examples * [Next.js](https://github.com/whopio/whop-nextjs-app-template/blob/main/app/discover/page.tsx) * [React Native](https://github.com/whopio/whop-sdk-ts/blob/main/packages/create-react-native/template/src/views/discover-view.tsx) # Experience View Source: https://docs.whop.com/apps/app-views/experience-view The experience view is what users see when they install your app into their Whop and click into it. This is the core functionality of your app - where users interact with your features and services. Experience View ## Configure 1. Go to the [developer dashboard](https://whop.com/dashboard/developer) 2. Create a new app or select an existing one 3. Scroll down to the **Hosting** section App Settings Enter your path for the experience view. The recommended default path is `/experiences/[experienceId]`. Experience View Path * `[experienceId]` is used to provide the accessed experience ID: `/experiences/[experienceId] -> /experiences/exp_***` * `[restPath]` is used for deep linking to specific sections of your app: `/experiences/[experienceId]/[restPath] -> /experiences/exp_***/posts/1` ## Preview Click the install button or copy the installation link and visit it in your browser. You will be prompted to install your app into your whop. Install App Button If you've already installed your app, you can access it from your whop. Whop Sidebar Apps Section 1. Open the dev tools by clicking the **cog** button 2. Set the environment to **localhost** ## Validate access Check if a user has access to an experience by using the SDK method [checkIfUserHasAccessToExperience](/sdk/api/access/check-if-user-has-access-to-experience). ```ts const access = await whopSdk.access.checkIfUserHasAccessToExperience({ experienceId: "exp_***", userId: "user_***", }); // No access // ^? { hasAccess: false, accessLevel: "no_access" } // Customer access // ^? { hasAccess: true, accessLevel: "customer" } // Admin access // ^? { hasAccess: true, accessLevel: "admin" } ``` See [Validate experience access](/sdk/validate-access#validate-experience-access) for more information. ## Examples * [Next.js](https://github.com/whopio/whop-nextjs-app-template/blob/main/app/experiences/%5BexperienceId%5D/page.tsx) * [React Native](https://github.com/whopio/whop-sdk-ts/blob/main/packages/create-react-native/template/src/views/experience-view.tsx) # Affiliate Commissions Source: https://docs.whop.com/apps/features/affiliate-commissions Earn affiliate commissions by linking to a Whop's discover page: ``` https://whop.com/discover/{companyRoute}/?app={yourAppId} ``` Available query parameters: * `?app={yourAppId}` sets your app as the affiliate * `?productId={accessPassId}` selects the product on the discover page (optional) # Login with Whop Source: https://docs.whop.com/apps/features/oauth-guide Learn how to implement Whop OAuth in a stand-alone application. ## Intro Use Whop OAuth to authenticate users in your web or iOS app. This guide only covers the basic steps to implement Whop OAuth and does not cover best practices regarding the OAuth2 protocol. It is recommended to use a library to handle the OAuth2 flow. We are going to release a guide on how to implement Whop OAuth with auth.js soon. ### Step 1: Create a Whop App and obtain secrets 1. Go to the [Whop Dashboard](https://whop.com/dashboard/developer/) and create a new app or select an existing one. 2. Add a redirect uri in your apps OAuth settings To test your app locally you can add a redirect uri on `http://localhost:{PORT}` but it is recommended to use https for production. 3. Copy the app id and api key and set them in your environment variables. Keep in mind that the api key is a secret and should not be shared with anyone. The app id is public and can be shared with anyone. ```.env NEXT_PUBLIC_WHOP_APP_ID=your-app-id WHOP_API_KEY=your-api-key ``` ### Step 2: Initiate the OAuth flow #### Setup the OAuth flow To follow this guide you will need to install the `@whop/api` package from npm: ```bash pnpm pnpm i @whop/api ``` ```bash npm npm i @whop/api ``` ```bash yarn yarn add @whop/api ``` Start off by creating a route that will be hit by the user when they click the `Login with Whop` button: ```ts /api/oauth/init/route.ts import { WhopServerSdk } from "@whop/api"; const whopApi = WhopServerSdk({ appApiKey: process.env.WHOP_API_KEY!, appId: process.env.NEXT_PUBLIC_WHOP_APP_ID, }); export function GET(request: Request) { const url = new URL(request.url); const next = url.searchParams.get("next") ?? "/home"; const { url, state } = whopApi.oauth.getAuthorizationUrl({ // This has to be defined in the redirect uris outlined in step 1.2 redirectUri: "http://localhost:3000/api/oauth/callback", // These are the authorization scopes you want to request from the user. scope: ["read_user"], }); // The state is used to restore the `next` parameter after the user lands on the callback route. // Note: This is not a secure way to store the state and for demonstration purposes only. return Response.redirect(url, { headers: { "Set-Cookie": `oauth-state.${state}=${encodeURIComponent( next )}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600`, }, }); } ``` Read more about available scopes [here](/api-reference/graphql/scopes). #### Adding the `Login with Whop` button Now continue by adding a link to your app that will initiate the `Login with Whop` flow: ```html Login with Whop ``` Upon clicking the link the user will be redirected to the Whop OAuth page and is prompted to authorize your app. ### Step 3: Exchange the code for a token Upon successful authorization the user will be redirected to the redirect uri you specified in the query parameters with `code` and `state` query parameters: ```ts /api/oauth/callback/route.ts import { WhopServerSdk } from "@whop/api"; const whopApi = WhopServerSdk({ appApiKey: process.env.WHOP_API_KEY!, appId: process.env.NEXT_PUBLIC_WHOP_APP_ID, }); export function GET(request: Request) { const url = new URL(request.url); const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); if (!code) { // redirect to error page return Response.redirect("/oauth/error?error=missing_code"); } if (!state) { // redirect to error page return Response.redirect("/oauth/error?error=missing_state"); } const stateCookie = request.headers .get("Cookie") ?.split(";") .find((cookie) => cookie.trim().startsWith(`oauth-state.${state}=`)); if (!stateCookie) { // redirect to error page return Response.redirect("/oauth/error?error=invalid_state"); } // exchange the code for a token const authResponse = await whopApi.oauth.exchangeCode({ code, redirectUri: "http://localhost:3000/api/oauth/callback", }); if (!authResponse.ok) { return Response.redirect("/oauth/error?error=code_exchange_failed"); } const { access_token } = authResponse.tokens; // Restore the `next` parameter from the state cookie set in the previous step. const next = decodeURIComponent(stateCookie.split("=")[1]); const nextUrl = new URL(next, "http://localhost:3000"); // This is an example, you should not store the plain user auth token in a cookie in production. // After setting the cookie you can now identify the user by reading the cookie when the user visits your website. return Response.redirect(nextUrl.toString(), { headers: { "Set-Cookie": `whop_access_token=${access_token}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600`, }, }); } ``` ## Implementing with authentication frameworks ### Auth.js To implement Whop OAuth with auth.js you can use the `authJsProvider` method on the `WhopOAuth` class. Get started by setting up the relevant auth.js distribution: * Next.js ([Installation Guide](https://authjs.dev/getting-started/installation?framework=Next.js)) ([Example](https://github.com/whopio/whop-sdk-ts/tree/main/examples/oauth-nextjs)) via `next-auth@beta` * SvelteKit ([Installation Guide](https://authjs.dev/getting-started/installation?framework=SvelteKit)) via `@auth/sveltekit` * Qwik ([Installation Guide](https://authjs.dev/getting-started/installation?framework=Qwik)) via `@auth/qwik` * Express ([Installation Guide](https://authjs.dev/getting-started/installation?framework=Express)) via `@auth/express` Create a Whop OAuth provider and add it to your auth.js provider configuration: ```ts const WhopProvider = whopApi.oauth.authJsProvider({ scope: ["read_user"], }); const authConfig = { providers: [WhopProvider], // ... rest of your auth.js configuration }; ``` # Payments and payouts Source: https://docs.whop.com/apps/features/payments-and-payouts Use the API to collect payment from users or payout users. ## Collecting Payments First, create the charge on the server using the Whop API. Then you can either: 1. Open a modal in your app using the iframe SDK (recommended) 2. Redirect the user to Whop's checkout page ### 1. Create the charge on the server > This step will create a charge on the server and return the inAppPurchase object required for the next step. On the server, use the [chargeUser](/sdk/api/payments/charge-user) method to create a charge: ```typescript app/api/charge/route.ts import { whopSdk } from "@/lib/whop-sdk"; export async function POST(request: Request) { try { const { userId, experienceId } = await request.json(); const result = await whopSdk.payments.chargeUser({ amount: 100, currency: "usd", userId: userId, // metadata is information that you'd like to receive later about the payment. metadata: { creditsToPurchase: 1, experienceId: experienceId, }, }); if (!result?.inAppPurchase) { throw new Error("Failed to create charge"); } return Response.json(result.inAppPurchase); } catch (error) { console.error("Error creating charge:", error); return Response.json({ error: "Failed to create charge" }, { status: 500 }); } } ``` ### 2. Confirm the payment on the client > In this step the user will be prompted to confirm the previously created charge in a modal. This function requires the iFrame SDK to be initialized. See [**iFrame Overview**](/sdk/iframe-setup) for more information. Use the iframe SDK to open a payment modal: ```tsx React "use client"; import { useIframeSdk } from "@whop/react"; export default function PaymentButton({ userId, experienceId, }: { userId: string; experienceId: string; }) { const iframeSdk = useIframeSdk(); const [receiptId, setReceiptId] = useState(); const [error, setError] = useState(); async function handlePurchase() { try { // 1. Create charge on server const response = await fetch("/api/charge", { method: "POST", body: JSON.stringify({ userId, experienceId }), }); if (response.ok) { const inAppPurchase = await response.json(); // 2. Open payment modal const res = await iframeSdk.inAppPurchase(inAppPurchase); if (res.status === "ok") { setReceiptId(res.data.receipt_id); setError(undefined); } else { setReceiptId(undefined); setError(res.error); } } else { throw new Error("Failed to create charge"); } } catch (error) { console.error("Purchase failed:", error); setError("Purchase failed"); } } return ; } ``` ```tsx Vanilla JS import { iframeSdk } from "@/lib/iframe-sdk"; const paymentButton = document.querySelector("button#payment-button"); const receiptElement = document.querySelector("span#receiptContainer"); const errorElement = document.querySelector("span#errorContainer"); function setError(error?: string) { if (errorElement instanceof HTMLSpanElement) { errorElement.textContent = error ?? ""; } } function setReceiptId(receiptId?: string) { if (receiptElement instanceof HTMLSpanElement) { receiptElement.textContent = receiptId ?? ""; } } if (paymentButton instanceof HTMLButtonElement) { paymentButton.addEventListener( "click", async function onPaymentButtonClick() { const userId = this.dataset.userId; const experienceId = this.dataset.experienceId; if (!userId || !experienceId) { throw new Error("Missing userId or experienceId"); } try { // 1. Create charge on server const response = await fetch("/api/charge", { method: "POST", body: JSON.stringify({ userId, experienceId }), }); if (response.ok) { const inAppPurchase = await response.json(); // 2. Open payment modal const res = await iframeSdk.inAppPurchase(inAppPurchase); if (res.status === "ok") { setReceiptId(res.data.receipt_id); setError(undefined); } else { setReceiptId(undefined); setError(res.error); } } else { throw new Error("Failed to create charge"); } } catch (error) { console.error("Purchase failed:", error); setError("Purchase failed"); } } ); } ``` ### 3. Validate the payment > After a payment is processed, you should validate it on your server using webhooks to ensure the payment was successful and update your application accordingly. Set up a webhook route to handle payment events: ```typescript app/api/webhook/route.ts import { makeWebhookValidator, type PaymentWebhookData } from "@whop/api"; import { after } from "next/server"; const validateWebhook = makeWebhookValidator({ webhookSecret: process.env.WHOP_WEBHOOK_SECRET, }); export async function POST(request: Request) { // Validate the webhook to ensure it's from Whop const webhook = await validateWebhook(request); // Handle the webhook event if (webhook.action === "payment.succeeded") { after(handlePaymentSucceededWebhook(webhook.data)); } // Make sure to return a 2xx status code quickly. Otherwise the webhook will be retried. return new Response("OK", { status: 200 }); } async function handlePaymentSucceededWebhook(data: PaymentWebhookData) { const { id, user_id, subtotal, amount_after_fees, metadata, ... } = data; // ... } ``` See [**Webhooks**](/apps/features/webhooks) for more information. Make sure to create a webhook in your [dashboard](https://whop.com/dashboard/developer/) app settings and set your `WHOP_WEBHOOK_SECRET` environment variable. Webhook Configuration ## Sending Payouts You can send payouts to any user using their Whop username. The funds will be transferred from your company's ledger account. ### Transfer Funds ```typescript import { whopSdk } from "@/lib/whop-sdk"; async function sendPayout( companyId: string, recipientUsername: string, amount: number ) { // 1. Get your company's ledger account const experience = await whopSdk.experiences.getExperience({ experienceId }); const companyId = experience.company.id; const ledgerAccount = await whopSdk.companies.getCompanyLedgerAccount({ companyId, }); // 2. Pay the recipient await whopSdk.payments.payUser({ amount: amount, currency: "usd", // Username or ID or ledger account ID of the recipient user destinationId: recipientUsername, // Your company's ledger account ID that can be retrieve from whopSdk.companies.getCompanyLedgerAccount() ledgerAccountId: ledgerAccount.company?.ledgerAccount.id!, // Optional transfer fee in percentage transferFee: ledgerAccount.company?.ledgerAccount.transferFee, }); } ``` # Create forum post Source: https://docs.whop.com/apps/features/post-to-feed Create a forum and a post using the API ## Overview To post in a forum, you must: 1. Find or create a *Forum Experience* 2. Create a *Forum Post* inside the *Forum Experience* If you already know what forum experience you want to post in, you can skip step 1, and use the experience ID directly in step 2. *** ## Find or create a forum experience A forum post must be created within a **Forum Experience**. The `findOrCreateForum` method will find an existing forum experience with the specified name, or create a new one with all the specified options ```typescript const newForum = await whopSdk .withUser("YOUR_AGENT_USER_ID") .forums.findOrCreateForum({ experienceId: experienceId, name: "Dino game results", whoCanPost: "admins", // optional: // expiresAt: Date.now() + 24 * 60 * 60 * 1000, // price: { // baseCurrency: "usd", // initialPrice: 100, // } }); ``` > This will create the forum in the same whop as the supplied experience. *** ## Create a forum post. Once you have the `experienceId` from the above, use it to create a post. ### Basic Forum Post ```ts const forumPost = await whopSdk .withUser("YOUR_AGENT_USER_ID") .forums.createForumPost({ forumExperienceId: newForum.createForum?.id, title: "Welcome!", content: "Excited to kick things off in our new forum 🎉", }); ``` * `withUser()`: ID of the user posting * `forumExperienceId`: The ID of the target forum * `title` and `content`: Main post body. *(title is optional)* ### Forum post with advanced options This demonstrates a rich post using all features: ```ts const forumPost = await whopSdk .withUser("YOUR_AGENT_USER_ID") .forums.createForumPost({ forumExperienceId: "exp_XXXXXX", // Visible even before purchase. title: "Big Launch + Community Poll!", // Visible only after purchase content: "Hidden content unless purchased. 🔒", // Add media to the post. // Learn how to upload in the upload-media section attachments: [ { directUploadId: "XXXXXXXXXXXXXXXXXXXXXXXXXX", }, ], // Do not send a notification to everyone about this post. isMention: false, // Lock the content and attachments behind a // one time purchase in the price + currency. paywallAmount: 9.99, paywallCurrency: "usd", // Add a poll to the post. poll: { options: [ { id: "1", text: "New Product Features" }, { id: "2", text: "Exclusive AMA" }, { id: "3", text: "Member Giveaways" }, ], }, }); ``` # Send push notification Source: https://docs.whop.com/apps/features/send-push-notification Send a push notification to a user or a group of users. ## Send a notification to everyone in an experience. Make sure you have [setup your whop SDK client on the server](/sdk/whop-api-client) ```typescript import { whopSdk } from "@/lib/whop-sdk"; // This could be a server action / api route that create a // piece of content in your app for this experience export async function createSomeContentInExperience( experienceId: string, content: string, // this could be a more complex type depending on your specific app. createdByUserId: string ) { // Create the content in your app. // ... await some database call etc etc... // Send a push notification to everyone in the experience. await whopSdk.notifications.sendPushNotification({ title: "New content is available" /* Required! */, content: content.slice(0, 100) + "...", // Format the content as you wish. experienceId, // send to all users with access to this experience. isMention: false, // Set this to true to make everyone immediately // get a mobile push notification. senderUserId: createdByUserId, // This will render this user's // profile picture as the notification image. }); } ``` See the full list of accepted parameters [here](/sdk/api/notifications/send-push-notification). ## Adding a deeplink to the notification. When you send a notification, you will usually want to send the user to specific section in your app upon clicking the notification. You can do this by using the `restPath` property. 1. Update your app path in the dashboard to handle the additional parameters. In the hosting section, set the "App path" field to something like: `/experiences/[experienceId]/[restPath]` 2. In your `sendPushNotification` call, add the `restPath` property. ```typescript await whopSdk.notifications.sendPushNotification({ title: "New content is available", content: content.slice(0, 100) + "...", experienceId, restPath: `/posts/${somePostId}`, // The specific posts route is just an example // You could also just add a query param like this: // restPath: `?id=${specialId}`, }); ``` 3. Update you app to handle the following route: When clicking on a notification, the user will open this specific url on your app within the whop iframe. ``` https://your-domain.com/experiences/exp_123/posts/post_123 ``` If using NextJS, you can add a `page.tsx` file with the path: `app/experiences/[experienceId]/posts/[postId]/page.tsx` Note: the exact path will depend on the pathname structure you set in the `restPath` property. ## Sending a notification to company admins Your app may want to alert company admins only, not all members. Use the `companyTeamId` field instead of the `experienceId` when sending the notification. ```typescript await whopSdk.notifications.sendPushNotification({ title: "A member just posted a new listing. Review it now.", content: `${listingTitle}`, companyTeamId, }); ``` You must send either the `companyTeamId` or the `experienceId` when sending a notification. Setting both will result in an error. ## Sending a notification to a specific subset of users. Use the `userIds` field to filter the users who will receive the notification. Whop will first apply either the `experienceId` or the `companyTeamId` filter and then apply the `userIds` filter Ensure that the `userIds` array contains valid user IDs that are part of the specified experience or company team. For example if you a building a bidding app you may want to alert the highest bidder if they were just outbid. ```typescript await whopSdk.notifications.sendPushNotification({ title: "You were just outbid", content: `${listingTitle}`, experienceId, // the experience ID that the current item is listed within. userIds: [oldHighestBidderUserId], }); ``` # Subscriptions Source: https://docs.whop.com/apps/features/subscriptions Gate your app behind a subscription or one-time purchase ## Setup your access pass on the dashboard. 1. Go to the your [app's dashboard](https://whop.com/dashboard/developer). 2. Select the access passes tab and create an access pass. Give it a name like "My App Premium" 3. Create a pricing plan for the access pass by clicking the "Add Pricing" button from the table row. 4. After creating the pricing plan, copy the plan id from the 3 dot menu in the pricing plan card. 5. Also copy the access pass id from the 3 dot menu in the access pass table row. We recommend storing the access pass id and plan id in environment variables for your app. Eg: ```bash NEXT_PUBLIC_PREMIUM_ACCESS_PASS_ID="prod_XXXXXXXX" NEXT_PUBLIC_PREMIUM_PLAN_ID="plan_XXXXXXXX" ``` ## Check if users have access When a user makes a request to your app, you can easily check if they have access using the whop api. ```typescript const hasAccess = await whopSdk.access.checkIfUserHasAccessToAccessPass({ accessPassId: process.env.NEXT_PUBLIC_PREMIUM_ACCESS_PASS_ID, // from step 5 above. userId: userId, }); ``` If a user does not have access, you can [prompt them to purchase](#collect-payment-from-users) or show a lite "free" version of the app to upsell them. ## Collect payment from users This function requires the iFrame SDK to be initialized. See [**iFrame Overview**](/sdk/iframe-setup) for more information. Use the iframe sdk to collect payment from users. This will show a whop native payment modal in which the user can confirm their purchase. ```tsx React "use client"; import { useIframeSdk } from "@whop/react"; export default function GetAccessButton() { const iframeSdk = useIframeSdk(); const [receiptId, setReceiptId] = useState(); const [error, setError] = useState(); async function handlePurchase() { try { const res = await iframeSdk.inAppPurchase({ planId: process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID }); if (res.status === "ok") { setReceiptId(res.data.receipt_id); setError(undefined); } else { setReceiptId(undefined); setError(res.error); } } catch (error) { console.error("Purchase failed:", error); setError("Purchase failed"); } } return ; } ``` ```tsx Vanilla JS import { iframeSdk } from "@/lib/iframe-sdk"; const getAccessButton = document.querySelector("button#get-access-button"); const receiptElement = document.querySelector("span#receiptContainer"); const errorElement = document.querySelector("span#errorContainer"); function setError(error?: string) { if (errorElement instanceof HTMLSpanElement) { errorElement.textContent = error ?? ""; } } function setReceiptId(receiptId?: string) { if (receiptElement instanceof HTMLSpanElement) { receiptElement.textContent = receiptId ?? ""; } } if (getAccessButton instanceof HTMLButtonElement) { getAccessButton.addEventListener( "click", async function onGetAccessButtonClick() { try { const res = await iframeSdk.inAppPurchase({ planId: process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID, }); if (res.status === "ok") { setReceiptId(res.data.receipt_id); setError(undefined); } else { setReceiptId(undefined); setError(res.error); } } catch (error) { console.error("Purchase failed:", error); setError("Purchase failed"); } } ); } ``` # Attaching custom metadata to a subscription You can attach custom metadata to a subscription by using the `createCheckoutSession` mutation. For example, you can use this to associate a subscription with an experience or company that it was created for. Using this you can attribute the source of the subscription and build powerful revenue sharing features into your app. Before using the `iframeSdk.inAppPurchase` function, you need to create a checkout session, and pass it to the function. ### Create the checkout session in a server action. Use the whopSdk to create a checkout session on your backend, pass the experienceId to this function. ```typescript import { whopSdk } from "@/lib/whop-sdk"; import { headers } from "next/headers"; export async function createSubscription(experienceId: string) { const { userId } = await whopSdk.verifyUserToken(await headers()); // Check to make sure the current user has access to the experience. const hasAccess = await whopSdk.access.checkIfUserHasAccessToExperience({ userId, experienceId, }); const checkoutSession = await whopSdk.payments.createCheckoutSession({ planId: process.env.NEXT_PUBLIC_PREMIUM_PLAN_ID, metadata: { experienceId, }, }); return checkoutSession; } ``` ### Pass the checkout session to the iframeSdk.inAppPurchase function. ```tsx React "use client"; import { useIframeSdk } from "@whop/react"; import { createSubscription } from "@/lib/actions/create-subscription"; export default function GetAccessButton({ experienceId, }: { experienceId: string; }) { const iframeSdk = useIframeSdk(); const [receiptId, setReceiptId] = useState(); const [error, setError] = useState(); async function handlePurchase() { try { const inAppPurchase = await createSubscription(experienceId); const res = await iframeSdk.inAppPurchase(inAppPurchase); if (res.status === "ok") { setReceiptId(res.data.receipt_id); setError(undefined); } else { setReceiptId(undefined); setError(res.error); } } catch (error) { console.error("Purchase failed:", error); setError("Purchase failed"); } } return ; } ``` This custom metadata will be available in the webhook payloads sent to your server (if enabled). You can use the `payUser` mutation to share your subscription revenue with the creator of the experience. # Upload media Source: https://docs.whop.com/apps/features/upload-media Use Whop to upload images, videos, audio, and other files. ### Client-Side: Set up the Image Upload Component First, create a component to handle image uploads. This example uses `react-dropzone` for the file upload interface. ```typescript import { useState, useCallback, useEffect } from "react"; import { useDropzone } from "react-dropzone"; function ImageUploader() { // Set up state for the image file and preview const [image, setImage] = useState<{ file: File; preview: string; } | null>(null); // Clean up object URLs when component unmounts useEffect(() => { const objectUrl = image?.preview; if (objectUrl) { return () => { URL.revokeObjectURL(objectUrl); }; } }, [image?.preview]); // Handle file drops const onDrop = useCallback((acceptedFiles: File[]) => { const file = acceptedFiles[0]; if (file) { setImage({ file, preview: URL.createObjectURL(file), }); } }, []); // Configure dropzone const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { "image/*": [".jpeg", ".jpg", ".png", ".gif"], }, maxFiles: 1, }); return (
{image?.preview ? ( Preview ) : (

Drag & drop an image here, or click to select

)}
); } ``` ### Server-Side: Handle File Uploads Create an API route to handle the file upload using the Whop SDK: ```typescript import { whopSdk } from "@/lib/whop-sdk"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; export async function POST(request: Request) { try { // Verify user authentication const headersList = await headers(); const userToken = await whopSdk.verifyUserToken(headersList); if (!userToken) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Get the file from the request const file = await request.blob(); // Upload to Whop const response = await whopSdk.attachments.uploadAttachment({ file: new File([file], `upload-${Date.now()}.png`, { type: "image/png", }), record: "forum_post", // or other record types }); // The response includes the directUploadId and URL return NextResponse.json({ success: true, attachmentId: response.directUploadId, url: response.attachment.source.url, }); } catch (error) { console.error("Error uploading file:", error); return NextResponse.json( { error: "Failed to upload file" }, { status: 500 } ); } } ``` ### Using Uploaded Media After uploading, you can use the attachment ID in various Whop features. For example, to create a forum post with the uploaded image (server-side): ```typescript const createForumPost = async (attachmentId: string) => { const post = await whopSdk.forums.createForumPost({ forumExperienceId: "your-forum-id", content: "Check out this image!", attachments: [{ directUploadId: attachmentId }], }); return post; }; ``` ### Supported File Types The Whop API supports the following file types for upload: * Images: `.jpg`, `.jpeg`, `.png`, `.gif` * Videos: `.mp4`, `.mov` * Documents: `.pdf` ### Best Practices 1. **File Size**: Keep uploads under 100MB for optimal performance 2. **Image Optimization**: Consider using libraries like `sharp` for image processing before upload 3. **Error Handling**: Implement proper error handling on both client and server 4. **Clean Up**: Remember to clean up any preview URLs to prevent memory leaks 5. **Security**: Always verify user authentication before handling uploads 6. **Progress Tracking**: Consider implementing upload progress tracking for better UX ### Complete Example Here's a complete example showing both client and server integration: ```typescript // app/components/MediaUploader.tsx (Client) import { useState, useCallback, useEffect } from "react"; import { useDropzone } from "react-dropzone"; export default function MediaUploader() { const [image, setImage] = useState<{ file: File; preview: string; } | null>(null); const [isUploading, setIsUploading] = useState(false); useEffect(() => { return () => { if (image?.preview) { URL.revokeObjectURL(image.preview); } }; }, [image]); const onDrop = useCallback((acceptedFiles: File[]) => { const file = acceptedFiles[0]; if (file) { setImage({ file, preview: URL.createObjectURL(file), }); } }, []); const { getRootProps, getInputProps } = useDropzone({ onDrop, accept: { "image/*": [".jpeg", ".jpg", ".png", ".gif"], }, maxFiles: 1, }); const handleUpload = async () => { if (!image?.file) return; setIsUploading(true); try { // Send to your API route const formData = new FormData(); formData.append("file", image.file); const response = await fetch("/api/upload", { method: "POST", body: formData, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error); } // Clear the form after successful upload setImage(null); } catch (error) { console.error("Upload failed:", error); } finally { setIsUploading(false); } }; return (
{image?.preview ? ( Preview ) : (

Drag & drop an image here, or click to select

)}
{image && ( )}
); } ``` This implementation provides a complete media upload solution with: * Drag and drop interface * File preview * Upload handling * Progress states * Error handling * Automatic cleanup # Webhooks Source: https://docs.whop.com/apps/features/webhooks Use webhooks to get notified when specific events happen relating to your app. *** ## Create a webhook 1. Go to the [developer dashboard](https://whop.com/dashboard/developer) 2. Create a new app or select an existing one 3. Click on **Webhooks** Webhook Settings 1. Click on **Create webhook** 2. Set your endpoint URL Webhook URL When testing locally, you'll need to tunnel your requests to your localhost endpoint. You can use [ngrok](https://ngrok.com/) to do this. 3. Select the events you want to receive and click on **Save** Webhook Events Copy the webhook secret by clicking on it and safely store it in your environment variables. Webhook Secret ## Handle webhook events Set up a webhook route to handle the selected events: ```typescript app/api/webhook/whop/route.ts import { makeWebhookValidator, type PaymentWebhookData } from "@whop/api"; import { after } from "next/server"; const validateWebhook = makeWebhookValidator({ webhookSecret: process.env.WHOP_WEBHOOK_SECRET, }); export async function POST(request: Request) { // Validate the webhook to ensure it's from Whop const webhook = await validateWebhook(request); // Handle the webhook event if (webhook.action === "payment.succeeded") { after(handlePaymentSucceededWebhook(webhook.data)); } // Make sure to return a 2xx status code quickly. Otherwise the webhook will be retried. return new Response("OK", { status: 200 }); } async function handlePaymentSucceededWebhook(data: PaymentWebhookData) { const { id, user_id, subtotal, amount_after_fees, metadata, ... } = data; // ... } ``` ## Test webhook events You can send dummy webhook events to your endpoint for testing purposes. Test Webhook Popup Test Webhook # Connect to websocket Source: https://docs.whop.com/apps/features/websocket-guide Learn how to implement real-time features using Whop's websocket API You can connect to the websocket from your client side frontend code running in the iFrame. ## Client Setup ### React When using react, it is recommended to use the `WhopWebsocketProvider` provider from the `@whop/react` package to connect to the websocket. 1. Mount the `WhopWebsocketProvider` provider: ```tsx app/layout.tsx import { WhopWebsocketProvider } from "@whop/react"; import { handleAppMessage } from "@/lib/handle-websocket-message"; export default function Layout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` 2. Handle app messages: ```tsx lib/handle-websocket-message.tsx export function handleAppMessage(message: proto.common.AppMessage) { console.log("Received app message:", message); // message.isTrusted is true if and only if the message was sent from your server with your private app API key. // message.json is the JSON string you sent from your server / client. // if you sent the message from the client using websocket.broadcast, // message.fromUserId will include the user id of the user who sent the message. } // You can also handle messages using the `useOnWebsocketMessage` hook. export function MyNestedReactComponent() { const [state, setState] = useState(""); useOnWebsocketMessage((message) => { setState(message.json); }); return
{state}
; } ``` 3. Handle connection status changes: ```tsx import { useWebsocketStatus } from "@whop/react"; // inside of a component const connectionStatus = useWebsocketStatus(); ``` ### Other frameworks Alternatively, you can create the websocket client using the `@whop/api` package in any framework. 1. Create the websocket client: ```typescript import { WhopClientSdk } from "@whop/api"; const whopApi = WhopClientSdk(); const websocket = whopApi.websockets.client({ joinExperience: "exp_XXXX", // optional, you can join a specific experience channel (ie, the one you are currently viewing). joinCustom: "some_custom_channel", // optional, you can join a custom channel. }); ``` 2. Add event handlers for messages: ```typescript websocket.on("appMessage", (message) => { console.log("Received custom message:", message); // message.isTrusted is true if and only if the message was sent from your server with your private app API key. // message.json is the JSON string you sent from your server / client. // if you sent the message from the client using websocket.broadcast, // message.fromUserId will include the user id of the user who sent the message. }); ``` 3. Handle connection status changes: ```typescript websocket.on("connectionStatus", (status) => { console.log("Websocket Status Updated:", status); }); websocket.on("connect", () => { console.log("Websocket Connected"); }); websocket.on("disconnect", () => { console.log("Websocket Disconnected"); }); ``` 4. Connect to the websocket and start receiving events: ```typescript websocket.connect(); ``` 5. *Optional:* Disconnect from the websocket: ```typescript websocket.disconnect(); ``` ## Send messages from the client You can send messages from the client to the server by using the `websocket.broadcast` or `useBroadcastWebsocketMessage` function. 1. Create a websocket client as above. 2. Send a custom message via websocket. ```tsx React import { useBroadcastWebsocketMessage } from "@whop/react"; export function SendMessageExample() { const broadcast = useBroadcastWebsocketMessage(); function sendMessage () { broadcast({ message: JSON.stringify({ hello: "world" }), target: "everyone", }); } return } ``` ```typescript Other frameworks // make sure you are connected by calling `websocket.connect()` websocket.broadcast({ message: JSON.stringify({ hello: "world" }), target: "everyone", }); ``` The target field is the same as the one you would pass to `whopApi.websockets.sendMessage` on the server. ## Send messages from your server You can broadcast trusted websocket messages from your server to connected clients by using the `whopApi.websockets.sendMessage` function. 1. Construct an instance of the whop server sdk and pass your API key: ```typescript import { WhopServerSdk } from "@whop/api"; const whopApi = WhopServerSdk({ appApiKey: process.env.WHOP_API_KEY, appId: process.env.NEXT_PUBLIC_WHOP_APP_ID, }); ``` 2. Send a custom string message via websocket. ```typescript // Send to all users currently on your app across all experiences / views. whopApi.websockets.sendMessage({ message: JSON.stringify({ hello: "world" }), target: "everyone", }); // send to all users currently on this experience // (only works if the experience belongs to your app) whopApi.websockets.sendMessage({ message: JSON.stringify({ hello: "world" }), target: { experience: "exp_XXXX" }, }); // create a custom channel that your websocket client can subscribe to. // Only works if when connecting on the client, you pass the same custom channel name. whopApi.websockets.sendMessage({ message: JSON.stringify({ hello: "world" }), target: { custom: "some_custom_channel" }, }); // send to a specific user on your app whopApi.websockets.sendMessage({ message: JSON.stringify({ hello: "world" }), target: { user: "user_XXXX" }, }); ``` ## Receive messages on your server Before you start, make sure you are using NodeJS 22.4 or higher, or Bun to run your server. Use the server websocket API to receive events such as chat messages as forum posts for a particular user on your server. You can use these events to build real-time apps such as chat bots and AI-agents that react to events on the platform. 1. Construct (or reuse) an instance of the whop server sdk and pass your API key: ```typescript import { WhopServerSdk } from "@whop/api"; const whopApi = WhopServerSdk({ appApiKey: process.env.WHOP_API_KEY, appId: process.env.NEXT_PUBLIC_WHOP_APP_ID, }); ``` 2. Create your websocket client and add handlers for messages / status changes: ```typescript const websocket = whopApi // Pass the user id of the user you want to receive events for .withUser("user_v9KUoZvTGp6ID") // Construct the websocket client .websockets.client(); ``` 3. Add event handlers for messages: ```typescript websocket.on("message", (message) => { console.log("Received Message:", message); const chatMessage = message.feedEntity?.dmsPost; if (chatMessage) { // handle the chat message } const forumPost = message.feedEntity?.forumPost; if (forumPost) { // handle the forum post } }); ``` 4. Add event handlers for status changes (same as client API): ```typescript websocket.on("connectionStatus", (status) => { console.log("Websocket Status Updated:", status); }); // Or you can also listen to the connect and disconnect events: websocket.on("connect", () => { console.log("Websocket Connected"); }); websocket.on("disconnect", () => { console.log("Websocket Disconnected"); }); ``` 5. Connect to the websocket and start receiving events: ```typescript websocket.connect(); ``` 6. *Optional:* Disconnect from the websocket: ```typescript websocket.disconnect(); ``` # Get an API key Source: https://docs.whop.com/apps/get-api-key All requests to Whop APIs are managed using a secure API key. 1. Go to [https://whop.com/dashboard/developer/](https://whop.com/dashboard/developer/). 2. Click the **Create app** button. 3. Give your app a name and click **Save**. 4. Copy the API key from the `Environment variables` section and use it in your code.