Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ERROR with Stripe Checkout: One-Time + Subscription Payment Buttons on the same page?

UPDATED I built a pricing page that uses Stripe Checkout to use both a One-Time payment button for product 1 and a Subscription payment button for product 2.

enter image description here

My goal is to redirect the one time payment button to Stripe Checkout with a one time payment, and separately redirect the subscription payment to a checkout with a recurring payment.

According to STRIPE this can be done using Subscription as the Mode in the CheckoutSession in create-checkout-session.php (sample project) :

The mode of the Checkout Session. Required when using prices or setup mode. Pass subscription if the Checkout Session includes at least one recurring item.

Contrary to the Stripe Docs the following line of code: 'mode' => 'subscription', activates subscription payments ONLY, but it doesnt redirect one time payments. For one-time payments to work I must change it to: 'mode' => 'payment', but then subscription payments don't work.

Here's the php code in question:

 <?php
    
    require_once 'shared.php';
    
    $domain_url = $config['domain'];
    
    // Create new Checkout Session for the order
    // Other optional params include:
    // [billing_address_collection] - to display billing address details on the page
    // [customer] - if you have an existing Stripe Customer ID
    // [payment_intent_data] - lets capture the payment later
    // [customer_email] - lets you prefill the email input in the form
    // For full details see https://stripe.com/docs/api/checkout/sessions/create
    
    // ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
    $checkout_session = \Stripe\Checkout\Session::create([
        'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
        'cancel_url' => $domain_url . '/canceled.html',
        'payment_method_types' => ['card'],
        'mode' => 'subscription',
        'line_items' => [[
          'price' => $body->priceId,
          'quantity' => 1,
      ]]
    ]);
    
    echo json_encode(['sessionId' => $checkout_session['id']]);

And here's the javascript code:

// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId) {
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId
    })
  }).then(function(result) {
    return result.json();
  });
};

// Handle any errors returned from Checkout
var handleResult = function(result) {
  if (result.error) {
    var displayError = document.getElementById("error-message");
    displayError.textContent = result.error.message;
  }
};

/* Get your Stripe publishable key to initialize Stripe.js */
fetch("./config.php")
  .then(function(result) {
    return result.json();
  })
  .then(function(json) {
    var publishableKey = json.publishableKey;
    var subscriptionPriceId = json.subscriptionPrice;
    var onetimePriceId = json.onetimePrice;

    var stripe = Stripe(publishableKey);
    // Setup event handler to create a Checkout Session when button is clicked
    document
      .getElementById("subscription-btn")
      .addEventListener("click", function(evt) {
        createCheckoutSession(subscriptionPriceId).then(function(data) {
          // Call Stripe.js method to redirect to the new Checkout page
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });

    // Setup event handler to create a Checkout Session when button is clicked
    document
      .getElementById("onetime-btn")
      .addEventListener("click", function(evt) {
        createCheckoutSession(onetimePriceId).then(function(data) {
          // Call Stripe.js method to redirect to the new Checkout page
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });
      
  });

Is it even possible to have both one time payments and recurring payments on the same page with Stripe Checkout? How can I accomplish this?

Update according to Bemn:

$checkout_session = \Stripe\Checkout\Session::create([
  'success_url' => $domain_url . '/success.html?session_id={CHECKOUT_SESSION_ID}',
  'cancel_url' => $domain_url . '/canceled.html',
  'payment_method_types' => ['card'],
  'mode' => $body->mode
    'line_items' => [[
    'price' => $body->price_xxx,
    // For metered billing, do not pass quantity
    'quantity' => 1,
  ]],

  'line_items' => [[
    'price' => $body->price_zzz,
    // For metered billing, do not pass quantity
    'quantity' => 1,
  ]]
]);

echo json_encode(['sessionId' => $checkout_session['id']]);

And the JS:

// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId, mode) {
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId,
      mode: mode // <-- passing the mode, e.g. 'payment' or 'subscription'
    })
  }).then(function(result) {
    return result.json();
  });
};

And the HTML:

<div data-stripe-priceid="pricexxx" data-stripe-mode="payment" id="onetime-btn" class="bold mt-2 d-inline-block w-100-after-md max-width-xxs py-2 btn btn-secondary">Ore Time</div>
    
<div data-stripe-priceid="pricexxx" data-stripe-mode="subscription" id="subscription-btn" class="bold mt-2 d-inline-block w-100-after-md max-width-xxs py-2 btn btn-secondary">Ore Time</div>
like image 627
ChosenJuan Avatar asked Dec 02 '25 06:12

ChosenJuan


1 Answers

Is it even possible to have both one time payments and recurring payments on the same page with Stripe Checkout?

Yes. The key is you should pass the correct options to generate the corresponding Stripe Checkout session ID.

How can I accomplish this?

  • Backend: Have a function to accept Stripe's price ID and payment mode as input and return a Stripe Checkout session ID as the output.

  • Frontend: Pass payment mode information to /create-checkout-session.php. (see the Note if you are unable to do so)


Details

The following solution assuming that:

  1. You generate a Stripe Checkout Session ID at the backend. That ID will then pass to .createCheckoutSession() in js frontend.
  2. You have a 1-time product (let's call it PAY) and a recurrent subscription (let's call it SUB).

Frontend

I think you are close. What you need to do is passing the mode information to your API endpoint as well:

// Create a Checkout Session with the selected plan ID
var createCheckoutSession = function(priceId, mode) { // <-- add a mode parameter
  return fetch("./create-checkout-session.php", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      priceId: priceId,
      mode: mode // <-- passing the mode, e.g. 'payment' or 'subscription'
    })
  }).then(function(result) {
    return result.json();
  });
};

If so, each checkout button in the page should have corresponding info of the priceId and payment mode. You can do so by storing them using data attribute:

<div data-stripe-priceid="price_yyy" data-stripe-mode="subscription">Recurrent</div>
<div data-stripe-priceid="price_zzz" data-stripe-mode="payment">1-time</div>

If so, you can get the data attributes by e.g. a click event.

Note: If you cannot add an extra param to indicate mode, you neeed to identify if the given price ID is a 1-time or recurrent product in the backend. See https://stripe.com/docs/api/prices/object?lang=php#price_object-type for more details.

Backend

Here are 2 sample code snippets from Stripe's documentation. Direct copying of them does not work.

Reference for PAY: https://stripe.com/docs/checkout/integration-builder

$checkout_session = \Stripe\Checkout\Session::create([
  'payment_method_types' => ['card'],
  'line_items' => [[
    'price_data' => [
      'currency' => 'usd',
      'unit_amount' => 2000,
      'product_data' => [
        'name' => 'Stubborn Attachments',
        'images' => ["https://i.imgur.com/EHyR2nP.png"],
      ],
    ],
    'quantity' => 1,
  ]],
  'mode' => 'payment',
  'success_url' => $YOUR_DOMAIN . '/success.html',
  'cancel_url' => $YOUR_DOMAIN . '/cancel.html',
]);

In your case, you may not need to define 'price_data'. Instead, you should use 'price', like the next example.

Reference for SUB: https://stripe.com/docs/billing/subscriptions/checkout#create-session

$checkout_session = \Stripe\Checkout\Session::create([
  'success_url' => 'https://example.com/success.html?session_id={CHECKOUT_SESSION_ID}',
  'cancel_url' => 'https://example.com/canceled.html',
  'payment_method_types' => ['card'],
  'mode' => 'subscription',
  'line_items' => [[
    'price' => $body->priceId,
    // For metered billing, do not pass quantity
    'quantity' => 1,
  ]],
]);
  1. Take a look at this reference: https://stripe.com/docs/api/checkout/sessions/create. For line_items, you can just simply using 'price' and pass the price ID (e.g. price_xxx), which means your 'line_items' will looks like this:
'line_items' => [[
  'price' => $body->priceId,
  'quantity' => 1,
]],

For 'mode', use the value from your API request. It should be something like:

'mode' => $body->mode

Which means in your backend you better define a function (e.g. generate_checkout_session) to:

  • parse the json body received in the API request
  • get priceId and mode from the parsed data
  • use the priceId and mode in \Stripe\Checkout\Session::create and
  • returns the checkout_session ID

Hope this (and the reference urls) can help you.

like image 102
Bemn Avatar answered Dec 03 '25 20:12

Bemn



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!