Stripe Subscriptions Integration with Payment Intents API

For the last couple years we with the team have built tons of different payment integrations for our clients. In this tutorial I would like to share with you my experience of developing Stripe subscriptions integration for WordPress (or pretty much any PHP website).

I am also going to use modern Stripe Payment Intents API in this guide. If you want to learn a little bit more about payment intents and why it is so cool to use them, I recommend to take a look at my another tutorial.

In order to make this tutorial simple I included only the very basics which are necessary to make the subscriptions work. Please don’t forget about validations, processing errors and protection against card testing attacks for example. If you need some help with it, you can always contact us and we will develop everything for you the best possible way.

And now let’s dive into the tutorial!

1. Create Products in Stripe Dashboard

The first thing we need to do when working with Stripe subscription is to create subscription products in Stripe dashboard. In order to do so you just need go to Products > Add product.

The only thing to keep in mind here is that the product price should be recurring:

creating recurring product in Stripe dashboard

Actually all this product-creating-stuff we need just because of a single reason – further in the code we will need to provide price_id parameter, which is only available after creating a product in Stripe dashboard:

product price ID (API ID) in Stripe
Price ID (Product API ID)

2. Create a Subscription Using Payment Intents (Collecting Payment Details before Creating an Intent)

The key here is to get customer payment details before creating an intent. I got some inspiration from official Stripe documentation but updated it specifically for subscriptions and for WordPress.

Once again I would like to remind you that the code is super-simplified and it doesn’t have any validation included and isn’t protected anyhow against card testing attacks. So you can either do everything by yourself or our team is always ready to help you with that.

Ok, let’s dive into it step by step.

2.1 – Payment form (HTML)

Everything just begins with a simple HTML form. We have already created it in payment intents tutorial, so right now let me just copy and paste it right here:

<form id="payment-form">
	<input type="email" name="email" placeholder="Your email *" required />
  	<div id="payment-element"></div>
	<button id="submit">Place order</button>
	<div id="error-message"></div>

I have also included an email field because we will definitely need it when we are going to create a customer.

2.2 – stripe.js

Stripe library stripe.js is mandatory when you are about to create a custom designed checkout form with credit card fields etc.

So you need to load it. And because currently we’re focused on WordPress, we can include it the following way, using wp_enqueue_scripts hook.

add_action( 'wp_enqueue_scripts', 'rudr_stripe_js_library' );

function rudr_stripe_js_library() {
	wp_enqueue_script( 'stripejs', '' );

2.3 – Form submit JavaScript event

I guess this part may seem a little bit complicated, so let’s break it down into four parts. So far we need to do the following:

  1. Display(load) Stripe payment form,
  2. Create a submit event and check for basic errors (simple ones, like card expiration year etc),
  3. Send an asynchronous request to our server (in order to create a customer and a subscription in PHP in the next step),
  4. Obtain a clientSecret and confirm payment.

Here is how to do it:

// 1. Display Stripe payment form
const stripe = Stripe( 'API PUBLIC KEY' )
const elements = stripe.elements( {
	mode: 'subscription',
	amount: productPrice * 100,
	currency: 'usd',
	//paymentMethodTypes: [ 'card' ],
} )
const paymentElement = elements.create( 'payment' )

paymentElement.mount( '#payment-element' )

// 2. Creating a submit event and checking for errors
const form = document.getElementById( 'payment-form' )
form.addEventListener( 'submit', async (event) => {
	const { error: submitError } = await elements.submit()
	if( submitError ) {
		console.log( submitError )

	// 3. Send an asynchronous request to our server
	const response = await fetch( ajaxurl + '?action=subscription', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		body: JSON.stringify({
			priceId: priceId,
			productId: productId,

	const { success, data } = await response.json()
	// 4. Obtain a clientSecret and confirm payment.
	if( true == success ) {
		const clientSecret = data.clientSecret;
		const { error } = await stripe.confirmPayment({
			confirmParams: {
				return_url: '',
				payment_method_data : {
					billing_details : {

Let’s break this code down:

  • We have some undefined variables in this code – productPrice, priceId and productId – don’t forget to define them first! In case you’ve forgotten, we have created a price ID in the very first step of this guide.
  • For an AJAX URL I am using ajaxurl + '?action=subscription' because I suppose here, that we are creating a custom integration for a WordPress website, which means that the AJAX URL is going to be something like /wp-admin/admin-ajax?action=. We’re passing an action parameter as a query argument here because it won’t be available in $_POST or $_REQUEST arrays so easy, so better use $_GET in this case. But if you’re creating your asynchronous requests with jQuery (and there is nothing wrong with that), just don’t worry about it and feel free to pass an action value inside the body of the request.

2.4. Create customers and subscriptions (PHP + WordPress)

Now it is time to process the AJAX request we’ve just sent and we’re going to do it WordPress-way, using wp_ajax_ and wp_ajax_nopriv_ action hooks.

So below is the code for your functions.php or whatever:

add_action( 'wp_ajax_subscription', 'rudr_create_stripe_subscription' );
add_action( 'wp_ajax_nopriv_subscription', 'rudr_create_stripe_subscription' );

function rudr_create_stripe_subscription(){
	// $_POST variable is available in jQuery $.ajax requests by the way
	$_POST = json_decode( file_get_contents( "php://input" ), true );

	require_once( __DIR__ . '/stripe/init.php');

	$stripe = new \Stripe\StripeClient( 'API SECRET KEY' );
	// creating a customer
	$customer = $stripe->customers->create(
			'email' => $_POST[ 'customerEmail' ],

	$subscription = $stripe->subscriptions->create(
			'customer' => $customer->id,
			'currency' => 'usd',
			'metadata' => array(
				'product_id' => $_POST[ 'pluginId' ],
			'items' => array(
					'price' => $_POST[ 'priceId' ],
			// this subscription is not paid yet
			'payment_behavior' => 'default_incomplete',
			'payment_settings' =>  array( 'save_default_payment_method' => 'on_subscription' ),
			'expand' => array( 'latest_invoice.payment_intent' ),

	if( isset( $subscription->latest_invoice->payment_intent->client_secret ) ) {
		wp_send_json_success( array( 'clientSecret' => $subscription->latest_invoice->payment_intent->client_secret ) );

If something doesn’t seem clear for you, you can always find a detailed explanation in the official Stripe documentation, though I’d rather highlight a couple moments for you:

  • We don’t need line 7 if you’re sending an AJAX request via jQuery. You can just remove it and forget about it.
  • On line 9 we included Stripe PHP library, you can also use composer for that.
  • This code doesn’t have any validations, error processing etc, but it should! Don’t forget to include everything before you’re about to use it on your live project.
  • I have also added a product ID into the subscription metadata (lines 24-26). We will need it in the next step when processing a webhook.

3. Stripe Subscriptions Webhooks

If you check the official Stripe documentation, you will find a lot of webhooks there, related to subscriptions. But in my opinion the best one to use is invoice.paid. In our specific case this webhook is going to run when:

  • A customer purchases a product with a subscription,
  • Subscription is automatically renewed.

Which seems to be perfect!

Wait, but how to find the difference between the two events mentioned above? Easy:

// that's our invoice object
$invoice = $event->data->object;

if( 'subscription_cycle' == $invoice->billing_reason ) {
	// automatic subscription renewal

if( 'subscription_update' == $invoice->billing_reason ) {
	// new subscription

Don’t forget to create this webhook in your Stripe dashboard Developers > Webhooks.

invoice.paid Stripe webhook

More details about processing a webhook you can find here.

If you need to develop any payment integration, please feel free to contact us.

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.

Need some developer help? Contact me

Follow me on X