1
2 Comments

5 Caveats of Integrating Stripe Payments to a Web Product

Recently, I've been working on integrating payment with my SaaS product PPResume. I've been researching for a long time, and finally decided to use stripe. I found some caveats during the integration process, and I'd like to share them with you for your reference.

The Choice of Four Integration Approaches

Stripe officially provides [four integration approaches] (https://docs.stripe.com/payments/accept-a-payment/web/compare-integrations):

  • payment links: the simplest one, with the lowest integration cost, but also the lowest degree of customizability, which is more suitable for simple business.
  • checkout, which is divided into hosted checkout and embedded checkout:
    • hosted checkout: the user needs to be redirected to the stripe's website during the checkout, like saying you need to be redirected to the Alipay or WeChat payment website when you buy something on Taobao and pay, the user process is a bit longer and the integration cost is not too high, it's relatively simple.
    • embedded checkout: stripe provides an embedded form, which can be directly embedded into your website, so that when users checkout, they don't need to be redirected to stripe, the user experience will be better, the integration cost is a lot higher than the hosted checkout
  • elements: the most customizable one, but also the most expensive one, all elements, such as credit card input box can be customized, suitable for highly complex business, not recommended for ordinary users to use

Next.js has a demo, you can visually compare the difference between payment links, checkout and elements.

My personal recommendation:

  • If the business is extremely simple, or the development time or ability is limited, just use payment links, for example, if you sell e-books or software licenses on the Internet, put a payment links, and when you receive the payment, you can send the e-books or software licenses to the customers automatically or manually; payment links have another significant advantage, that is, they can be used to send the e-books or software licenses to the customers automatically or manually. Another significant advantage of payment links is that because the payment links themselves provide a URL, you can share this URL to various platforms, just like the QR code we use every day.
  • If you have a slightly more complex business, such as multiple pricing plans, you can use checkout, and if you want the best user experience and can afford the higher development costs, you can use embedded checkout, but checkout has a small pitfall of associated customers, which will be discussed later.
  • elements is not recommended, the development cost is too high.

Problems with Stripe Pricing Table

Stripe provides a no code pricing table, you can easily embed it in your website. However, Stripe's pricing table has a problem, by default, when clicking the CTA button (in this case it is the 'Subscribe' button) , it will be directed to stripe's checkout page, whereas for general SaaS products, Pricing Table's CTA button will be directed to a different page depending on whether or not the user is already logged in:

  • If the user is not yet logged in, it will redirect to the user's registration page to guide the user to register and log in.
  • If the user is already logged in, it follows the normal process and redirects to the subscribe payment page.

Another issue with Stripe's Pricing Table is that the UI is still not as customizable as it should be, with no separate CSS customization available, which could be a big difference from the product's own UI style.

There is a specialized Pricing Table SaaS product that partially solves this problem, you can refer to it. If you can't, you can just write a Pricing Table by your own, it won't be too hard.

Better to Associate Checkout Session with a Customer

The create API for Stripe Checkout Session receives a customer parameter, which is the id of the customer built into stripe. If this customer parameter is not provided when creating a Stripe Checkout Session (stripe.checkout.sessions.create), then Stripe's backend will group all customers with the same email/credit card/phone into a single guest customer, which would be visible only in Stripe's Dashboard, but not in Stripe API, and there is no way for the guest customer to interface with Customer Portal (about Customer Portal, see later).

My personal recommendation:

  • When creating a stripe checkout session, create the stripe customer first, however, don't create multiple Stripe customers with a single customer email, make sure that one customer email creates and only one Stripe Customer. The code is similar to this:
/**
 * Retrieves or creates a Stripe customer
 *
 * This function first attempts to fetch the user's context usign a customer
 * email.
 *
 * Then checks if a customer already exists in Stripe with that email, if
 * existed, return that stripe customer, otherwise a new customer is created in
 * Stripe.
 *
 * [@param](/param) {NextRequest} request - The incoming request object from Next.js.
 *
 * [@returns](/returns) {Promise<Stripe.Customer | null>} A promise that resolves to the
 * Stripe Customer object if found or created, otherwise null.
 */
export async function getOrCreateCustomer(
  request: NextRequest
): Promise<StripeServer.Customer | null> {
  const stripe = getStripeServer();

  try {
    // find a way to get a customer email from your auth system
    const customerEmail = "[email protected]";

    if (!customerEmail) {
      return null;
    }

    const customers = await stripe.customers.list({
      email: customerEmail,
      limit: 1,
    });

    if (customers.data.length === 0) {
      return await stripe.customers.create({
        email: customerEmail,
      });
    } else {
      return customers.data[0];
    }
  } catch (err) {
    return null;
  }
}

Better to Create Customer Portal with API

Stripe provides a no code Customer Portal, which allows users to manage payment related data by themselves, such as payment methods, subscription records, invoices and so on. These things are tedious and boring to implement on your own, so it would be very convenient if the payment system can provide a corresponding solution.

As far as I know, Stripe and LemonSqueezy provide customer portal, while Paddle doesn't (there is another dedicated SaaS product that provides a Customer Portal for Paddle).

There are two ways to integrate with the Stripe Customer Portal:

  • One is to activate the no-code link directly in the Stripe Dashboard. After the activation, you get a URL, put that URL in your website, and when the user accesses that URL, they can log in with a verification code to access or manage payment-related information.
  • Another way is through the API , when the user accesses the customer portal, the API temporarily creates a URL for the customer portal, and then returns this temporary URL to the user. The and the user accesses the customer portal through this temporarily created URL. The biggest benefit of this approach is that users don't have to go through a verification code to log into the customer portal, the user experience is far more better than the first approach. Code looks like:
/**
 * Creates a Stripe customer portal session and redirects user to that portal
 *
 * It first retrieves or creates a Stripe customer based on the incoming
 * request, then generates a session for the Stripe Billing Portal, and finally
 * redirects the user to the Stripe Billing Portal.
 *
 * The major benefit of using API to create a customer portal with customer
 * attached over plain customer portal link provided from stripe dashboard is,
 * user won't need to manually sign in for API created customer portal.
 * Basically, when you create a customer portal with API, it will generate a
 * short-lived URL for user and this URL do not need user to sign in.
 *
 * From [Stripe](https://docs.stripe.com/api/customer_portal/sessions):
 *
 * ```
 * A portal session describes the instantiation of the customer portal for a
 * particular customer. By visiting the session’s URL, the customer can manage
 * their subscriptions and billing details. For security reasons, sessions are
 * short-lived and will expire if the customer does not visit the URL. Create
 * sessions on-demand when customers intend to manage their subscriptions and
 * billing details
 * ```
 *
 * [@param](/param) {NextRequest} request - The incoming request object from Next.js.
 *
 * [@returns](/returns) {Promise<Response>} A promise that resolves to a redirect response
 * to the Stripe Billing Portal, or a JSON response indicating that the customer
 * was not found.
 */
export async function GET(request: NextRequest) {
  const stripe = getStripeServer();
  const customer = await getOrCreateCustomer(request);

  if (!customer) {
    return Response.json(
      {
        message: "Customer not found",
      },
      { status: 404 }
    );
  }

  const customerPortalSession = await stripe.billingPortal.sessions.create({
    customer: customer.id,
    return_url: getAbsoluteUrlWithDefaultBase(routes.pages.settings.billing),
  });

  return Response.redirect(customerPortalSession.url, 303);
}

The Implementation of Stripe Webhook

After the user places an order, the system needs to fulfill the user's order needs, for example, if your SaaS is a e-commerce shop, then the merchant needs to pack and ship the goods, while if it is a virtual service such as SaaS, it is generally necessary to grant the user the appropriate Pro privileges, this process is called [order fulfillment](https://docs.stripe.com/ checkout/fulfillment).

Order fulfillment requires the implementation of a webhook:

Webhooks are required

You can’t rely on triggering fulfilment only from your Checkout landing page,
because your customers aren’t guaranteed to visit that page. For example,
someone can pay successfully in Checkout and then lose their connection to the
internet before your landing page loads.

Set up a webhook event handler to get Stripe to send payment events directly
to your server, bypassing the client entirely. Webhooks are the most reliable
way to know when you get paid. If webhook event delivery fails, we retry several
times.

The so-called webhook, in essence, it is a public exposed API endpoint from your system, when the user placed an order successfully, Stripe will send a POST request to this API endpoint, with a request event that includes a detailed order payment information. The webhook will receives this information and then decide how to fulfill this order accordingly.

Webhook implementation has a small caveats, that is, Stripe does not guarantee the timing order of the event, nor does it guarantee the uniqueness of the request—in fact, there is no way to make such guarantee in the public web request. Therefore, in your Webhook implementation, depending on the requirements, if there is a high demand for data consistency, it is best to implement idempotency.

Conclusion

Stripe's development experience is very good, but the product matrix is very large, so newbies tend to get confused easily. There are some hidden caveats as mentioned above.

All in all, even with so much SDKs, platforms, integrating the payment system is still very difficult and boring. I would like to write a “Stripe 101: The Missing Tutorial for Indie Makers” once I finish the payment integration for my own SaaS.

One more thing, my product, PPResume, is about to launch a payment plan recently after polishing for a year. All users that sign up before the payment launch would get 50% off discount in one year. Thank you!

posted to Icon for group Money
Money
on September 17, 2024
  1. 1

    The idempotency point is the one that bites hardest in production. The Stripe-level key isn't enough — you need idempotency enforced on your own DB writes and side effects too. Otherwise a network timeout between your write and your event dispatch still leaves you with inconsistent state.

  2. 1

    Great rundown. One caveat worth adding as a #6: failed subscription payment handling.

    Once you've integrated Stripe subscriptions, the next silent problem is what happens when a payment fails. Stripe's default behavior: 3-4 automatic retries over a few days, then the subscription cancels. The customer gets a Stripe-branded email (if that), but no proactive outreach from you.

    For recurring products, this is where 20-40% of involuntary churn comes from — not customers who chose to leave, but customers whose cards failed silently.

    The fix is to listen for the invoice.payment_failed webhook and send a Day 1 / Day 3 / Day 7 recovery email sequence yourself. Day 1 is informational (no pressure), Day 3 adds urgency, Day 7 is the final notice. Recovery rates of 30-60% on failed invoices are realistic.

    Worth wiring this up at the same time as your initial Stripe integration — it's a small addition but high ROI.

Trending on Indie Hackers
I'm a lawyer who launched an AI contract tool on Product Hunt today — here's what building it as a non-technical founder actually felt like User Avatar 151 comments Never hire an SEO Agency for your Saas Startup User Avatar 85 comments A simple way to keep AI automations from making bad decisions User Avatar 65 comments “This contract looked normal - but could cost millions” User Avatar 54 comments 👉 The most expensive contract mistakes don’t feel risky User Avatar 41 comments We automated our business vetting with OpenClaw User Avatar 34 comments