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:

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:

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>
</form>
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', 'https://js.stripe.com/v3/' );
}
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:
- Display(load) Stripe payment form,
- Create a
submit
event and check for basic errors (simple ones, like card expiration year etc), - Send an asynchronous request to our server (in order to create a customer and a subscription in PHP in the next step),
- 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) => {
event.preventDefault()
const { error: submitError } = await elements.submit()
if( submitError ) {
console.log( submitError )
return
}
// 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,
customerEmail: form.email.value,
}),
})
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({
elements,
clientSecret,
confirmParams: {
return_url: 'https://rudrastyh.com/thank-you',
payment_method_data : {
billing_details : {
email: form.email.value
}
}
}
})
}
})
Let’s break this code down:
- We have some undefined variables in this code –
productPrice
,priceId
andproductId
– 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 anaction
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(
array(
'email' => $_POST[ 'customerEmail' ],
)
);
$subscription = $stripe->subscriptions->create(
array(
'customer' => $customer->id,
'currency' => 'usd',
'metadata' => array(
'product_id' => $_POST[ 'pluginId' ],
),
'items' => array(
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.

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
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