Published on January 13, 2025
Accepting Payments with Stripe and Remix
When you’re building a product, especially if it’s your first time, one of the toughest things to tackle is accepting payments. I’ve previously set up both Stripe and Paddle for my products and have found both to be quite tedious and time-consuming to grasp and implement. I have writen a separate post comparing these payments processors that you can check out if you’re interested in learning more about this.
In this post, I’ll walk you through setting up Stripe payments in a Remix application hoping to make it easier for you to understand the high level process of accepting payments and subscriptions.
Setting Up the Upgrade Button
To start accepting subscriptions, you’ll need to create a trigger for the user to initiate the payment process. This is usually a button on your billing or subscription page that the user can click to start the payment process.
<button id="upgrade">Upgrade</button>
In our case, we’ll keep it simple with a button that the user can click to start the payment process. In a real-world scenario, you’ll want to give them the option to choose between different subscription plans and billing cycles.
Creating a Stripe Session and Redirecting
If you have bought something online, you have likely seen Stripe’s checkout page before. It’s the page that appears when you’re about to pay for something. We’re going to create one for our customer who has just clicked the upgrade button.
To do this, we’ll use Remix’s actions. Remix actions handle server-side logic. We’ll use an action to create a Stripe session and redirect the user to Stripe’s checkout page.
Action Setup
Server-Side Code: In your Remix route, define an action:
import { Stripe } from 'stripe';
import { redirect } from '@remix-run/node';
export async function action() {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'subscription',
line_items: [
{
price: 'your_price_id_here',
quantity: 1
}
],
success_url: `${YOUR_DOMAIN}/success`,
cancel_url: `${YOUR_DOMAIN}/cancel`
});
return redirect(session.url);
}
- Replace
your_price_id_here
with your actual price ID from your Stripe dashboard. - Replace
YOUR_DOMAIN
with your site’s domain.
Form Submission: Wrap your button in a form to trigger the action:
<form method="POST">
<button type="submit" id="upgrade">Upgrade</button>
</form>
This form submission will execute the defined action on the server. If everything goes well, the user will be redirected to Stripe’s checkout page and will be able to complete the payment process.
Webhooks for Payment Confirmation
Most payment processors send webhooks to your server when a payment is successful or failed. This is how Stripe works, and it’s a way to get real-time updates on the status of a payment.
Before setting up webhooks, it’s helpful to understand how they work.
What are Webhooks?
Webhooks are messages or HTTP callbacks sent from an application when a specific event occurs. Instead of your application constantly checking for updates, webhooks push updates to your server automatically. When an event like a payment confirmation or cancellation happens on Stripe’s end, they’ll send a POST request to an endpoint you specify. This allows your application to react immediately to changes, keeping your user data and application state up-to-date without any delay. It’s an efficient way to handle real-time events, ensuring your app responds quickly to user actions or system changes.
Now that we understand how webhooks work, let’s set them up.
Setting Up Webhooks with Stripe
Go to your Stripe dashboard. Look for the “Webhooks” section and add a new endpoint. This endpoint should point to one of your server routes, for example, /webhook. This URL is where Stripe will send event notifications. Make sure you’ve noted down the webhook secret provided by Stripe, as you’ll need it for verifying the webhook’s authenticity.
Next, you’ll need to create a route in Remix to handle these webhook events. Here’s how you might set it up:
import type { ActionFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export const action: ActionFunction = async ({ request }) => {
const sig = request.headers.get('stripe-signature');
const body = await request.text();
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
console.error(`Webhook Error: ${err.message}`);
return json({ error: 'Webhook error' }, { status: 400 });
}
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
// Fulfill the purchase, update user state, etc.
break;
case 'customer.subscription.created':
// Handle new subscription, possibly update user's access or permissions
break;
case 'customer.subscription.deleted':
// Handle subscription cancellation, revoke access or permissions
break;
// Handle other event types
default:
console.log(`Unhandled event type ${event.type}`);
}
return json({ received: true });
};
-
Webhook Verification: Before processing any data from the webhook, we verify its signature. This step is crucial for security. Stripe includes a signature in the headers of each webhook request, which you verify using the STRIPE_WEBHOOK_SECRET. This ensures that the request actually came from Stripe and not from a malicious source trying to mimic Stripe.
-
Event Handling: The switch statement checks the type of event received. For each event type, you can perform actions like updating user records, sending notifications, or changing subscription statuses. Here, we’ve included examples for when a checkout session completes, a new subscription starts, or an existing subscription is canceled.
-
Default Case: It’s good practice to log events you haven’t explicitly handled yet. This allows you to expand your webhook handler in the future as you encounter new event types or need to react to different scenarios.
Verifying signatures is essential because it prevents your application from acting on potentially fraudulent or tampered data. Since webhooks involve sending potentially sensitive information over the internet, ensuring the authenticity of each event is a critical security measure. This verification step helps maintain the integrity of your application’s payment and subscription management processes.
Updating User State
After receiving confirmation from Stripe, you’ll need to update the user’s state. It’s helpful to store the subscription status, next billing date, and other relevant information in your database.
For this example, we’ll keep it simple and just set a flag in the database to indicate that the user has subscribed.
// Inside webhook handler for subscription events
if (event.type === 'customer.subscription.created') {
const subscription = event.data.object;
await updateUserSubscriptionInDatabase(subscription.customer, true);
}
When a user subscribes, the webhook will be triggered and the user’s subscription status will be updated in the database.
Post-Payment Handling
After processing a payment or receiving webhook events, your application needs to respond appropriately to ensure a smooth user experience and maintain data integrity. Here’s how to handle various post-payment scenarios:
Success Page
When a payment is confirmed by Stripe, the user is redirected to the success url set in the checkout.session.completed
webhook.
Loader Setup: Use Remix’s loader
function to fetch the latest user status and perform actions based on the successful payment:
import { redirect } from '@remix-run/node';
import { getUserFromSession } from '~/utils/auth'; // Assuming you have a utility for session
export async function loader({ request }) {
const user = await getUserFromSession(request);
if (!user) {
return redirect('/login'); // Redirect to login if no user
}
if (user.isSubscribed) {
return {
status: 'subscribed',
message: 'Thank you for subscribing! Your account is now upgraded.'
};
}
return {
status: 'not_subscribed',
message: 'Something went wrong with your subscription. Please try again.'
};
}
- This code checks if the user is logged in and redirects to login if not.
- It verifies the user’s subscription status, which should be updated by your webhook handler.
- It provides appropriate messages based on the subscription status.
UI Updates: On the success page, display:
- A personalized thank you message.
- Details about the subscription (e.g., plan name, billing cycle).
- Links to newly accessible features or areas of the site.
- A link to manage their subscription or view invoices.
Cancel Page
If a user cancels the payment process, they’re redirected to the cancel url set in the checkout.session.completed
webhook.
Redirect: Automatically redirect them back to the subscription page or homepage with a message:
import { redirect } from '@remix-run/node';
export async function loader() {
return redirect('/?message=Payment+canceled');
}
Message Display: Show a message explaining that the payment was canceled and encourage them to try again or explore alternative options.
Handling Subscription Changes
Webhooks notify you of initial subscriptions and subsequent changes.
Subscription Updates: When a user upgrades, downgrades, or their payment method fails, you’ll receive events like customer.subscription.updated
or invoice.payment_failed
.
- Action on Update: Update the user’s access level in your database or session to reflect their new subscription status.
- Send Notifications: Email or notify users about subscription changes, upcoming bills, successful renewals, or payment issues.
Subscription Cancellation: When a subscription is canceled (customer.subscription.deleted
):
- Revoke Access: Adjust the user’s permissions in your app to reflect they no longer have access to premium features.
- Inform User: Send an email or in-app notification explaining the cancellation and offering options like reactivation or support.
Testing with Stripe’s Test Credit Cards
When developing and testing your Stripe integration, you can use Stripe’s test credit card numbers to simulate various payment scenarios. Here are some commonly used test card numbers:
Scenario | Card Number |
---|---|
Successful Payment | 4242 4242 4242 4242 |
Authentication Required | 4000 0025 0000 3155 |
Card Declined | 4000 0000 0000 9995 |
Insufficient Funds | 4000 0000 0000 9995 |
Expired Card | 4000 0000 0000 0069 |
Incorrect CVC | 4000 0000 0000 0127 |
Note: For all scenarios except “Expired Card,” use any future date for the expiry date, any 3-digit number for the CVC, and any 5-digit number for the ZIP code. For the “Expired Card” scenario, use a past date for the expiry date.
These test cards allow you to simulate different scenarios and ensure your application handles each case correctly. Remember to use these test card numbers only in Stripe’s test mode and never in live mode.
For a full list of test card numbers and scenarios, refer to the Stripe Testing Documentation.
By using these test cards, you can confidently test your payment flows and ensure a smooth user experience before going live.
Conclusion
This setup allows your Remix application to integrate with Stripe for subscription management. Using actions for setup, webhooks for updates, and proper user state management ensures your application reacts to payment events effectively. Remember to test with Stripe’s test mode before going live.