Setup Stripe Payments on your Website with Payment Intents

Almost 7 years ago I created a tutorial about Stripe integration on a website. When I created it, it seemed quite difficult to me, but now it looks easy-breezy.

The thing is that it is obsolete right now. Well, it actually works for some cards, but mainly for those cards that are operating in North America. Today our goal is to implement it with brand new Stripe API which is called Payment Intents.

And I am going to guide your through it step by step.

If you don’t want to dive deep into the code, maybe me and my team could help you with some custom development?

1. Stripe PHP library

In order to use PHP methods in this tutorial, you have to include Stripe PHP library. First of all you can download the latest release from GitHub.

Unzip it to your website project and include init.php file. If you’re using a WordPress theme, this line of code could go to functions.php file:

require_once( get_stylesheet_directory() . '/stripe-php/init.php' );

For both WordPress themes and plugins:

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

Testing API call

Before proceeding to the next step, it would great to double check if everything is fine. In order to do that we can create a super-simple API request.

Go to Stripe Dashboard, turn on Test mode toggle if you haven’t done this yet and also find your API keys (Publishable and Secret) there.

Run this piece of code anywhere in your plugin or theme:

$stripe = new \Stripe\StripeClient(
  'sk_test_*******' // API Secret here
);

$stripe->customers->create( array(
  'description' => 'My First Test Customer',
) );

If everything goes well, and I am sure it should, you can find the customer in your Stripe Dashboard on Customers page.

creating first Stripe customer via API call

2. Payment Intents

How it works

Well, I could dive straight into the examples right now but let’s slow down for just a little bit. I want to explain you shortly what are payment intents.

Payment Intents are… your customers intentions to pay ;)

The best way to describe it I suppose is to describe it within a checkout process. Here it is step by step.

  1. A customer opens a checkout page, where a credit card form is going to be displayed.
  2. We immediately create a Payment Intent on that page and pass product price into it (and some custom data if needed).
  3. We also display a credit card form for the customer which is depended on Payment Intent customer ID.
  4. The customer submits the form and Stripe processes the payment asynchronously. It also returns the appropriate message on error or success.
  5. Do the rest stuff with webhooks.

Now let’s dive into it!

Creating a Payment Intent

Let’s assume we have some custom checkout page. It could be a WordPress custom page template, let’s say page-checkout.php.

Don’t forget to include Stripe PHP library if you haven’t done this yet.

$intent = $stripe->paymentIntents->create(
	array(
		'amount' => $price,
		'currency' => 'USD',
		'automatic_payment_methods' => array( 'enabled' => true ),
		'metadata' => array(
    		'product_id' => $product_id,
			'licence_type' => $license_type
  		),
	)
);

Now $intent->client_secret contains client secret – ID of a specific customer who is going to make a specific purchase.

Please note, that if $price variable is not provided you will receive a Fatal error. I also passed there some optional meta data, it could be anything.

Using Sessions to Avoid Duplicates

Even in the official Stripe documentation you could find a mention of creating a Payment Intent in PHP before the <form> element of the checkout. So, we are running $stripe->paymentIntents->create() on every page load. Maybe it is okay for some cases, but look what I found in my Stripe dashboard.

a lot of incomplete payments in Stripe Dashboard
Wow, one more incomplete payment on every page load.

So, I suppose we have to try creating payment intents only when necessary.

But how?

For example when you work with WooCommerce, you could try to connect it to a specific cart object. On my own website I don’t have a cart object, customers only choose a specific plugin and a specific license for that plugin, that’s all, two parameters.

So, how to store already created payment intent and where?

  • We can store it in PHP $_SESSION variable.
  • We can store it in JavaScript as well, either in cookies or local storage.

Which way is better? In PHP method we will create a payment intent directly in the checkout page code, which will increase the checkout page loading time, but only for the first time. This method is definitely simpler. If you’re going to use it in JavaScript, you need to send additional AJAX request to create a payment intent. A little bit more complex code, your customers will also have to wait while it loads, but it will be a preloader, the checkout page itself will be loaded fast every time.

Let’s do it the simple way ok? First things first, please add this line of code somewhere before any HTML is printed on the page session_start();.

The only thing you have to keep in mind is that our checkout session should be unique for three variables – $plugin_price, $plugin_id and $license_type. Why? Because we used all of them when creating a payment intent (check the code above). So, may our session variable contain a key like $_SESSION[ 'checkout'.$key ] and the key consists of "$plugin_price:$plugin_id:$license_type". Straight to the example now!

New payment intent code:

$key = "$plugin_price:$plugin_id:$license_type";
// do we have to create a payment intent for this specific key?
if( ! isset( $_SESSION[ 'checkout'.$key ] ) ) {

	$intent = $stripe->paymentIntents->create(
		array(
			'amount' => $plugin_price * 100, // in cents
			'currency' => 'USD',
			'automatic_payment_methods' => array( 'enabled' => true ),
			'metadata' => array(
				'product_id' => $plugin_id,
				'licence_type' => $license_type
			)
		)
	);

	$_SESSION[ 'checkout'.$key ] = $intent->client_secret;

}

$client_secret = $_SESSION[ 'checkout'.$key ];

Now you can go to checkout page, refresh it infinite amount of time and the payments won’t be duplicated in Stripe dashboard! Even if you are going to refresh it separately for different products, it also doesn’t create duplicates.

3. Create Credit Card form

The form itself is going to be loaded via <iframe> element. If you got used to tokens, don’t be upset too soon. The form is completely customisable. And it is also PCI compliant. It was quite easy for me to make the fields look exactly like all the fields on my website.

Basic HTML structure

It is suuuuuper simple.

<form id="payment-form">

  	<div id="payment-element"></div>
	
	<button id="submit">Place order</button>
	
	<div id="error-message"></div>
	
</form>
  • #payment-element – it is where Stripe will display card fields.
  • #error-message – it is where Stripe will display errors.

Loading the Form in JavaScript

Once HTML is ready, we have to load and display the credit card form. This process consists of two steps.

First step is to load Stripe JS library. Because my website is all about WordPress, here is a WordPress example how to do it:

add_action( 'wp_enqueue_scripts', function() {
	wp_enqueue_script( 'stripejs', 'https://js.stripe.com/v3/' );
} );

The second step is to use this JavaScript code:

const stripe = Stripe( 'PUBLISHABLE API KEY HERE' );
const options = {
  	clientSecret: 'CLIENT SECRET HERE',
}
const elements = stripe.elements( options );
const paymentElement = elements.create( 'payment' );

paymentElement.mount( '#payment-element' );

const form = document.getElementById( 'payment-form' );

form.addEventListener( 'submit', async (event) => {
	event.preventDefault();

	const {error} = await stripe.confirmPayment({
		elements,
		confirmParams: {
			return_url: 'https://rudrastyh.com/thank-you'
		},
	});

	if (error) {
		const messageContainer = document.querySelector('#error-message');
		messageContainer.textContent = error.message;
	} else {
		// Your customer will be redirected to your `return_url`
	}
});

Once done everything correctly, the form will appear on the page:

Stripe credit card form with default styles
Form fields have default Stripe styles. The button doesn’t have styles at all.

Appearance

In my opinion our next step is to do something with form styles. There is a specific documentation which is available on Stripe website. I will only show you how I managed to make it look just to fit my website design.

When passing options into stripe.elements, you can also add appearance key there. Like that:

const options = {
  	clientSecret: 'CLIENT SECRET HERE',
	appearance: {
		variables: {},
		rules: {}
	}
}
  • variables – here you can redefine some of the standard variables which will affect the whole form. The full list of variables – in Stripe docs.
  • rules – here you can add the whole set of CSS rules for a specific form element.

And here is my example:

const options = {
  	clientSecret: 'CLIENT SECRET HERE',
	appearance: {
		variables: {
			fontLineHeight: '1.4',
			fontSizeBase : '19px',
			colorText: '#5c574d',
			colorPrimary: '#27bff1',
			colorDanger: '#e68282',
			focusBoxShadow: '0 0 0 2px #92def7',
			borderColor: '#cccccc'
		},
		rules:{
			'.Input' : {
				border: '1px solid #ccc',
				boxShadow: 'none',
			}
		}
	}
}

After adding this (and also I’ve added a CSS-class to the <button> element), my form started to look like:

Custom appearance of the Stripe credit card form

Custom form fields

The last thing about the form I think I should definitely mention here is how to add any custom fields into it. For example what if you have to collect emails?

We have already passed product id in payment intent metadata. It would be great to pass customer email there as well, but it is not possible, unless your customer is a registered user on the website, so let’s add fields to the form.

Adding fields is simpler that you can imagine:

<form id="payment-form">

	<input type="text" id="fname" name="fname"/>
	<input type="email" id="email" name="email" />

And then do not forget to process their values:

const {error} = await stripe.confirmPayment({
	elements,
	confirmParams: {
		return_url: 'https://rudrastyh.com/thank-you',
		payment_method_data : {
			billing_details : {
				name : form.fname.value,
				email : form.email.value
			}
		}
	}
});

Or better collect payment details before creating an intent (creating intents using AJAX)

Now we have a payment form ready and what would you say if I suggest you to create a payment intent not on an every page load trying to making them unique by using sessions but after a customer clicks “Place order” button?

It is possible to achieve and can be done by sending one more AJAX request inside the submit() event before using stripe.confirmPayment() in this JavaScript snippet.

form.addEventListener( 'submit', async (event) => {
	event.preventDefault();

	// HERE 
	
	const {error} = await stripe.confirmPayment({

Oh and you need to provide mode=payment to the options object:

const options = {
	mode: 'payment',

Ok, now let’s do it!

form.addEventListener( 'submit', async (event) => {
	event.preventDefault();
	
	// here we validate card details and stuff
	const { error: submitError } = await elements.submit();
	if( submitError ) {
		console.log( submitError );
		return;
	}
	
	// then we're sending an AJAX request
	const response = await fetch( ajaxurl, {
		method: "POST",
		headers: { 'Content-Type': 'application/json' },
		body: JSON.stringify({
			productId: productId,
			licenseType: licenseType,
			customerEmail: form.email.value,
			customerName: form.fname.value
		}),
	});
	
	const { success, data } = await response.json();
	
	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 : {
						name : form.fname.value,
						email : form.email.value
					}
				}
			},
		});

Please note, that in the code above we have some variables which are not set, here are they: ajaxurl, productId, licenseType – it is up to you how to provide values to these variables. For example I am using WordPress which means that ajaxurl is going to be admin-ajax.php?action=intent (for example), but this allows us to process the AJAX request this way:

// add_action( 'wp_ajax_{ACTION}', ...
add_action( 'wp_ajax_intent', 'rudr_create_intent' );
add_action( 'wp_ajax_nopriv_intent', 'rudr_create_intent' );

function rudr_create_intent(){

	$_POST = json_decode(file_get_contents("php://input"), true);

	// I recommend to do a lot of validation stuff here
	
	require_once( __DIR__ . '/stripe/init.php');

	$stripe = new \Stripe\StripeClient( 'API SECRET KEY' );
		
	$intent = $stripe->paymentIntents->create(
		array(
			'amount' => $product_price * 100, // in cents
			'currency' => 'usd',
			'metadata' => array(
				'product_id' => $_POST[ 'pluginId' ],
				'license_type' => $_POST[ 'licenseType' ]
			)
		)
	);

	if( isset( $intent->client_secret ) ) {
		wp_send_json_success( array( 'clientSecret' => $intent->client_secret ) );
	}

	wp_send_json_error( new WP_Error( 'something_wrong', 'Something went wrong' ) );

}

Once again, it is up to you to decide how you would like to get $product_price, maybe you are using WooCommerce and you would like to use get_price() method of WC_Product etc.

4. Webhooks

Webhooks are just notifications from Stripe about a specific event once it happens. For example it could be a successful payment event or something else. Stripe will push that notification to a specific URL on your website. Your goal is to collect the event data and do something with it – maybe to provide an access to a product or to send an email or both.

Create a Webhook

First of all we have to create it inside your Stripe dashboard. In order to do that, go to Developers and then to Webhooks.

creating webhooks inside Stripe dashboard

The main thing you have to do is to specify webhook URL. It is URL on your website where Stripe is going to push some information about the purchase. For example I can set there https://rudrastyh.com/checkout?check_webhook=1 or just https://rudratyh.com/hook.php.

Next thing we could do is to select a specific event. For example we are working with Payment Intents right now, so proceed to Payment Intents category and select something you may need. I selected payment_intent.succeeded.

Selecting a specific webhook events

If you look on the right part of the screen you will see that Stripe suggests you the code for hook.php file.

I think it is almost all, do not close the Stripe dashboard yet because we will need webhook signing secret key.

Processing Webhooks

Below is the working code which I took from the official Stripe documentation. I think it could be used for as many webhooks as you wish, you can easily replace if with a switch PHP-statement.

require_once( __DIR__ . '/stripe/init.php');
// require_once( __DIR__ . '/wp-load.php' ); WordPress environment

\Stripe\Stripe::setApiKey('API SECRET KEY');

$endpoint_secret = 'WEBHOOK SIGNING SECRET KEY';

// some checks start here
$payload = @file_get_contents('php://input');
$event = null;

try {
	$event = \Stripe\Event::constructFrom( json_decode($payload, true) );
} catch(\UnexpectedValueException $e) {
	echo 'Webhook error while parsing basic request.';
	http_response_code(400);
	exit();
}

if ($endpoint_secret) {
	$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
	try {
		$event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $endpoint_secret );
	} catch(\Stripe\Exception\SignatureVerificationException $e) {
		echo 'Webhook error while validating signature.';
		http_response_code(400);
		exit();
	}
}
// checks ended

// handle the event
if( 'payment_intent.succeeded' === $event->type ) {

	$paymentIntent = $event->data->object; // contains a \Stripe\PaymentIntent
		
	// we can get some of the parameters we passed
	$plugin_id = $paymentIntent[ 'metadata' ]->product_id;
	$plugin_license = $paymentIntent[ 'metadata' ]->license_type;
	$amount = $paymentIntent[ 'amount_received' ] / 100;
	$billing_details = $paymentIntent[ 'charges' ]->data[0]->billing_details;
	$email = $billing_details->email;
	$name = $billing_details->name;
	$country_code = $billing_details->address->country;

	// Do anything you want, e.g. create orders, send emails etc.

}

http_response_code(200);

And that’s all, guys.

Also I can recommend you my tutorial about creating a payment gateway for WooCommerce.

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