To sell Discord roles with Stripe, you connect a Stripe payment to a bot that is allowed to manage roles in your server. Either a hosted tool does that on top of your Stripe account (LaunchPass, Upgrade.chat), or you run a small bot of your own that listens to Stripe's webhooks and adds or removes the role. Whop and Patreon take the payment themselves and run their own role bots, and Discord's Server Subscriptions let members pay inside Discord, but only for server owners based in the US.
The routes differ more on fees than on features. At 50 members paying $10 a month, you would pay between $32 and $79.50 a month in fees depending on the route, as of September 2026. Below are the numbers with the arithmetic, the Discord policy question to check before you build, and a tested webhook-to-role sketch you can run yourself.
Ways to sell Discord roles, and which fits your server
| If you... | Use |
|---|---|
| Want it live today with no code or hosting | LaunchPass or Whop |
| Want the lowest per-payment fee without writing code | Whop |
| Want payments to land in your own PayPal and Stripe accounts | Upgrade.chat |
| Already publish on Patreon and your members are there | Patreon's Discord integration |
| Are US-based and want members to pay inside Discord | Discord Server Subscriptions |
| Want your own Stripe customer list, custom rules, or several products and servers | Your own bot |
LaunchPass has a free plan, but it cannot charge members, so the paid Premium plan is the one to compare.
What each option costs at 50 and 300 members
The fee schedules
All figures are for US card payments as of September 2026, taken from each company's own pricing page (linked in the sources).
| Option | Per-payment fee | Fixed monthly |
|---|---|---|
| Your own Stripe bot | Stripe 2.9% + 30¢, plus Stripe Billing 0.7% (pay-as-you-go) | $0, plus hosting |
| Whop | 2.7% + 30¢, plus $0.07 Radar per transaction | $0 |
| LaunchPass Premium | 3.5%, plus Stripe 2.9% + 30¢ | $29 |
| Upgrade.chat Free | 5.9%, plus your Stripe 2.9% + 30¢ (up to 500 subscribers) | $0 |
| Upgrade.chat Pro / VIP / Max | 4.9% / 3.9% / 2.9%, plus your Stripe fees (up to 1,000 / 2,500 / unlimited subscribers) | $19 / $49 / $199 |
| Patreon (pages published after Aug 4, 2025) | 10%, plus 2.9% + 30¢ processing | $0 |
| Discord Server Subscriptions (desktop and web) | 6% processing, then 10% of what remains | $0 |
| Discord Premium Apps (for a bot's own features) | 6% processing, then 15% of what remains on the first $1M | $0 |
Upgrade.chat takes payments into your own Stripe or PayPal account, so those accounts' processing fees come on top of its percentage. I used Stripe's card rate for the math.
The results
| Option | Fee per $10 payment | 50 members ($500/mo) | 300 members ($3,000/mo) |
|---|---|---|---|
| Whop | $0.64 | $32.00 | $192.00 |
| Your own Stripe bot | $0.66 | $33.00 + hosting | $198.00 + hosting |
| Upgrade.chat Free | $1.18 | $59.00 | $354.00 |
| Upgrade.chat Pro | $1.08 + $19/mo | $73.00 | $343.00 |
| LaunchPass Premium | $0.94 + $29/mo | $76.00 | $311.00 |
| Discord Server Subscriptions | $1.54 | $77.00 | $462.00 |
| Patreon | $1.59 | $79.50 | $477.00 |
| Discord Premium Apps (reference) | $2.01 | $100.50 | $603.00 |
The arithmetic
Each line is one $10 payment, then multiplied by 50 and 300 members.
- Your own Stripe bot: $10 × 2.9% = $0.29, plus $0.30, plus $10 × 0.7% = $0.07, so $0.66. × 50 = $33.00. × 300 = $198.00.
- Whop: $10 × 2.7% = $0.27, plus $0.30, plus $0.07, so $0.64. × 50 = $32.00. × 300 = $192.00.
- LaunchPass: $10 × 3.5% = $0.35, plus Stripe's $0.59, so $0.94. × 50 = $47.00 + $29 = $76.00. × 300 = $282.00 + $29 = $311.00.
- Upgrade.chat Free: $10 × 5.9% = $0.59, plus Stripe's $0.59, so $1.18. × 50 = $59.00. × 300 = $354.00.
- Upgrade.chat Pro: $0.49 + $0.59 = $1.08. × 50 = $54.00 + $19 = $73.00. × 300 = $324.00 + $19 = $343.00.
- Patreon: $10 × 10% = $1.00, plus $0.29 + $0.30, so $1.59. × 50 = $79.50. × 300 = $477.00.
- Discord Server Subscriptions: processing $10 × 6% = $0.60, then 10% of the remaining $9.40 = $0.94, so $1.54. × 50 = $77.00. × 300 = $462.00.
- Discord Premium Apps: $0.60 processing, then 15% of $9.40 = $1.41, so $2.01, leaving the developer $7.99.
Upgrade.chat's Pro plan beats its free plan above $1,900 a month in sales. VIP ties Pro at $3,000 a month (300 members at $10) and beats it above that, and Max beats VIP above $15,000. The subscriber caps can force an upgrade sooner: 500 on the free plan and 1,000 on Pro.
What the table leaves out
- International cards and currency conversion. Stripe and Whop both add 1.5% for international cards and 1% when currency is converted. Patreon charges 2.5% when a member pays in a different currency from your payout currency.
- Disputes. Stripe charges $15 per dispute received and $15 more if you counter it; the second fee is refunded if you win. Whop charges $15 per dispute.
- Payouts. Whop charges $2.50 per next-day ACH payout and Patreon $0.25 per US bank payout. Discord pays monthly once your server has earned $100, then whenever at least $25 more has built up.
- Mobile. Server Subscriptions bought in Discord's iOS app cost the member more, because Apple's fee is added to the price. They can't be bought on Android at all.
- Other listed fees. Whop lists $0.03 per 3D Secure check and adds 0.5% when its Billing automation is enabled. Stripe Tax Basic's no-code option adds 0.5% per transaction where you are registered to collect tax.
- Stripe Billing on hosted tools. LaunchPass and Upgrade.chat charge through your Stripe account. LaunchPass lists only Stripe's card fee, neither says whether Stripe's 0.7% Billing fee also applies, and I couldn't confirm it either way. Check your first Stripe invoice.
Where your own bot breaks even
On fees alone, your own Stripe bot costs about the same as Whop: $0.66 against $0.64 per $10 payment. It only beats Whop below about $7.80 a payment, where Whop's flat 7¢ Radar fee outweighs Stripe's higher percentage, so the reasons to build are control, custom rules and owning the Stripe account.
Against LaunchPass it saves $0.28 per payment plus the $29 subscription: $43 a month at 50 members and $113 at 300. Against Patreon it saves $0.93 per payment, or $46.50 a month at 50 members. Hosting runs about $3 to $5 a month: Railway's Hobby plan is $5 including $5 of usage and can give the webhook an HTTPS domain, while a $3 bot host such as PebbleHost's 1 GB plan runs the bot but may not serve HTTPS. Weigh that saving against the time to build and maintain the bot, or what a developer charges. The Discord bot cost guide has real price ranges.
Discord's own options, and the rule that affects bots
Server Subscriptions
Server Subscriptions let members pay inside Discord for 1 to 3 monthly tiers that open up paid channels or the whole server. Owners must be 18 or older, have 2FA on, run a Community server and provide US banking and ID to Stripe. Discord's Creator Revenue FAQ says they will not be available outside the United States.
Discord keeps a 10% platform fee after a 6% processing fee on desktop and web. Free trials of one day or one week are built in. If you qualify, it is the lowest-effort option, and members never leave Discord.
Premium Apps
Premium Apps is Discord's store for a bot's own paid features: user subscriptions, server subscriptions and one-time purchases. Developers in the US, UK and EU keep 85% of what remains after processing on their first $1 million in sales, then 70%. The app must be verified and owned by a developer team. It is built for public bots selling features to many servers, not for a creator selling access to their own server.
The price-parity rule
Since October 7, 2024, Discord's Developer Policy has required apps that sell paid features, in regions where Premium Apps is supported, to also offer those features through Premium Apps at a final price no higher than anywhere else. Discord's FAQ says you can keep Stripe or Patreon alongside, but not at a lower price or with extra perks off Discord.
The policy and its FAQ talk about an app's own features: commands, content, enhanced functionality. A server selling access to its own community, with a bot only handing out the role, is not among Discord's examples, and I haven't found Discord guidance that addresses it directly. Read the current policy against what you sell, and if it is unclear, ask Discord developer support before you launch. When I build one of these, that check is part of the Blueprint.
How a Stripe-to-Discord role bot works
- The member signs in with Discord through OAuth2 with the
identifyscope, which gives you their Discord user ID. Add theguilds.joinscope if you want the bot to put them into the server after payment. - Your server creates a Stripe Checkout Session in subscription mode, with the Discord user ID in
client_reference_idand in the subscription's metadata. - Stripe sends
checkout.session.completed. The bot verifies the signature, reads the subscription, and adds the role through Discord's Add Guild Member Role endpoint. - Renewals and failures arrive as
customer.subscription.updated. While the status isactive,trialingorpast_due, the role stays. When it turnsunpaidorcanceled, orcustomer.subscription.deletedarrives, the role comes off. - A nightly job reconciles every active subscription against every member holding the role, to catch anything a missed webhook or a manual change left behind.
Linking Discord accounts to Stripe customers
This step decides whether the right person gets the role. There are three common ways, and one of them is clearly best:
- Discord sign-in before checkout proves which Discord account is paying. The ID travels with the purchase, so there is nothing to match later.
- Email matching, where the member types their Stripe email into a command, works if you verify the email. Without verification, anyone who knows a subscriber's email can claim their access.
- Asking for a username at checkout is the weakest. Usernames change, get mistyped and can be someone else's.
Grace periods without writing a timer
You don't need your own countdown. When a renewal fails, Stripe marks the subscription past_due and retries the card. Smart Retries' recommended default is 8 tries within 2 weeks, and you can pick 1 week to 2 months. Keeping the role during past_due is your grace period.
Then check one setting. In Stripe's failed-payment settings you choose what happens after the last retry: cancel the subscription, mark it unpaid, or leave it past due. Pick cancel or unpaid. If you leave it past due, a role that follows the status never comes off.
The core code: Stripe webhook to Discord role
This sketch is the core of a Stripe-to-Discord role bot. It verifies Stripe's signature, reads the current subscription, and adds or removes one role through Discord's REST API, with no gateway connection needed. I tested it with stripe 22.6.2 (API version 2026-08-26.dahlia), discord.js 14.27.0 and Express 5.2.1 on Node 24.15.
npm install stripe discord.js expressSTRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
DISCORD_TOKEN=your-bot-token
GUILD_ID=your-server-id
MEMBER_ROLE_ID=the-paid-role-id
STRIPE_PRICE_ID=price_...First, the checkout. This function runs after the member has signed in with Discord, and the ID it receives must come from Discord's API, never from a form field.
// Runs after the member has signed in with Discord (OAuth2, identify scope).
// discordUserId comes from GET /users/@me with their access token, never from a form field.
async function startCheckout(stripe, discordUserId) {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
client_reference_id: discordUserId,
subscription_data: { metadata: { discord_user_id: discordUserId } },
success_url: 'https://example.com/welcome',
cancel_url: 'https://example.com/join',
});
return session.url; // redirect the member here
}
module.exports = { startCheckout };Then the webhook. Putting the Discord ID on the subscription, not only on the Checkout Session, is what lets the cancellation event find the right member months later.
const express = require('express');
const Stripe = require('stripe');
const { REST, Routes, DiscordAPIError } = require('discord.js');
const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, DISCORD_TOKEN, GUILD_ID, MEMBER_ROLE_ID } = process.env;
const stripe = new Stripe(STRIPE_SECRET_KEY);
const rest = new REST({ version: '10' }).setToken(DISCORD_TOKEN);
// past_due keeps the role while Stripe retries the card: that is the grace period.
const HAS_ACCESS = new Set(['active', 'trialing', 'past_due']);
async function syncRole(subscriptionId) {
// Fetch the current subscription instead of trusting the event snapshot,
// because Stripe does not guarantee that events arrive in order.
const sub = await stripe.subscriptions.retrieve(subscriptionId);
const discordUserId = sub.metadata.discord_user_id;
if (!discordUserId) return;
const route = Routes.guildMemberRole(GUILD_ID, discordUserId, MEMBER_ROLE_ID);
const reason = `Stripe ${sub.id} is ${sub.status}`;
try {
if (HAS_ACCESS.has(sub.status)) await rest.put(route, { reason });
else await rest.delete(route, { reason });
} catch (err) {
// 10007 Unknown Member: they are not in the server yet. The daily sync job retries.
if (err instanceof DiscordAPIError && err.code === 10007) return;
throw err;
}
}
const app = express();
// express.raw keeps the body byte-for-byte, which signature verification needs.
app.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(req.body, req.headers['stripe-signature'], STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook signature check failed: ${err.message}`);
}
const obj = event.data.object;
try {
if (event.type === 'checkout.session.completed' && obj.mode === 'subscription') {
await syncRole(obj.subscription);
} else if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
await syncRole(obj.id);
}
res.sendStatus(200);
} catch (err) {
console.error(`Failed on ${event.id}`, err);
res.sendStatus(500); // Stripe retries failed deliveries
}
});
app.listen(process.env.PORT || 3000);Run it with node --env-file=.env webhook.js, which Node has supported since version 20.6.
A few details matter more than they look:
- The raw body. Stripe signs the exact bytes it sends. If a JSON parser touches the body first, verification fails, which is why this route uses
express.rawinstead ofexpress.json. - The five-minute window.
constructEventalso rejects signatures older than five minutes by default, which stops replayed requests. Keep your server's clock synced. - Idempotency. Adding a role a member already has, or removing one they don't, changes nothing, so a duplicate delivery is harmless. Stripe retries failed deliveries for up to three days in live mode, so duplicates do happen.
- Ordering. Stripe doesn't guarantee event order. Reading the subscription fresh means an old event can't undo a newer state.
- Discord's rules. The bot needs the Manage Roles permission, and its own role must sit above the paid role, because a bot can only grant roles below its highest role. The permissions calculator builds the invite link.
What I tested
I ran the files above exactly as published, with Stripe's generateTestHeaderString signing real payloads and the two network calls (Stripe's subscriptions.retrieve and Discord's REST put and delete) replaced by recorders, since nothing should call live APIs in a test. All 18 cases passed:
checkout.session.completedwith an active subscription adds the role and passes a reason for Discord's audit log.- A Checkout Session in payment mode is ignored.
trialingandpast_duekeep the role;unpaid,canceledandincompleteremove it or never grant it.- An event whose snapshot says active, for a subscription that is now canceled, removes the role.
- A duplicate delivery repeats the same request and returns 200.
- A subscription without
discord_user_idmetadata is left alone. - A member who isn't in the server (Discord error 10007) returns 200 instead of three days of Stripe retries.
- Any other Discord or Stripe error returns 500, so Stripe retries.
- A tampered body, a wrong signing secret, a missing
Stripe-Signatureheader and a ten-minute-old timestamp are all rejected with 400 before anything else runs, while a four-minute-old one is accepted.
I also ran checkout.js and the two Discord calls against local fake APIs to see the exact requests: the Checkout form fields, the role route, the X-Audit-Log-Reason header, and a 10007 response arriving as a DiscordAPIError. Both files type-check against stripe's bundled TypeScript types.
What the sketch leaves out
- The nightly reconciliation job. List active subscriptions from Stripe and the members holding the role from Discord, then fix the difference. Listing members needs the Server Members privileged intent. Since June 10, 2026, apps that reach fewer than 10,000 unique users switch it on in the Developer Portal and larger ones apply for review.
- An event log. Before adding side effects that aren't idempotent, such as a welcome message or an email, record each
event.idin a table with a unique constraint and skip ones you have seen. - Members with more than one subscription. Before removing the role, check that the Stripe customer has no other active subscription.
- Several tiers. Map each Stripe price ID to a role ID instead of using one
MEMBER_ROLE_ID. - Adding people to the server. With the
guilds.joinscope, Discord's Add Guild Member endpoint can add the buyer with the role already set; the bot needs Create Instant Invite for that. Discord's Developer Policy requires telling people clearly and getting their permission before their account is added to a server. - Disputes. Listen for
charge.dispute.createdif you want access to end when a chargeback opens.
Test it end to end in Stripe test mode
- Run
stripe listen --forward-to localhost:3000/stripe/webhookwith the Stripe CLI. It prints awhsec_secret for local testing; put that in.envand restart the webhook. - Create a Checkout Session with
checkout.jsusing test keys and pay with Stripe's test card 4242 4242 4242 4242. If that Discord account is in your test server, the role appears within seconds. - To test a failed renewal, create a test clock in the Dashboard, then a customer on that clock with the card 4000 0000 0000 0341, which attaches but declines every charge. Give the customer a subscription with a short free trial and your test member's ID as
discord_user_idmetadata. Advance the clock past the trial: the charge fails, the status goes topast_dueand the member has the role. Advance past the retry window and the status moves tocanceledorunpaid, and the role comes off.
stripe trigger customer.subscription.deleted is less useful here than it looks. It creates a new test subscription without your discord_user_id metadata, so the sketch correctly ignores it.
Setup checklist
- Discord: create the paid role, give the bot Manage Roles, and drag the bot's role above the paid role. If your server requires 2FA for moderation, the account that owns the bot application needs 2FA too. Adding the bot to your server covers the invite.
- Stripe: create the product and recurring price, add a webhook endpoint subscribed to
checkout.session.completed,customer.subscription.updatedandcustomer.subscription.deleted, set the final retry outcome to cancel or unpaid, and turn on the customer portal so members can cancel without messaging you. - Hosting: the webhook has to be reachable over HTTPS at all times. Hosting a Discord bot 24/7 compares the options.
When to hand it off
If one role on one Stripe price is all you need, the code above plus a nightly job gets you there. If you need tiers, a migration from Patreon or LaunchPass without members resubscribing, trials, or a dashboard, I build and run exactly this as a paid membership bot on your own Stripe account and Discord application. Send a brief and I'll reply with a fixed quote.