Paypal v Stripe: A developer's view
Tags: stripe, paypal, ipn
Published: 2021-04-06 12:43:06

Now that Stripe is becoming popular as an alternative to Paypal for collecting digital payments I though it would be useful to take one of my existing Paypal applications and redevelop it using Stripe. This post is far too long, but if you're interested in software design, I hope you may find it useful. In the course of writing it I found I'd never really understood how the Paypal design worked in the first place. Maybe you're in the same boat!

Since however I'm not completely confident that some of the things I've said are 100% accurate, I'd also be very pleased to hear comments and counter-views. 

The application I've chosen uses Paypal's IPN (Instant Payment Notification) system to take membership subscriptions and record customer details in a local database. Users like it but I have to say that as a developer I found it difficult to code. 

With IPN, the paypal button in the front end (written in html/javascript) submits a form that forwards the amount to be charged and my Paypal business address (my account id) to a script on your "back-end" server. In my case, since I want Paypal to tell me told about a successful payment, the form also includes the address which Paypal needs to use in order to give me the details I need for my database update. 

When the button is clicked, control is passed to Paypal's server and the collection of paypal a/c or credit card details is conducted completely outside the view of my application.

From my point of view, things only start to happen when Paypal contacts my back end software.  In my particulr case this back end is written in php and lives on a hostpapa server. 

In an IPN design, the back end consists of a Listener.php file (based on a "template" provided by Paypal) that sits at the address specified in the payment button, alongside a little bundle of php functions stored in another Paypal-provided file called paypalIPN.php. The purpose of these files is to make sure that the link between Paypal and yourself is watertight and tamper-proof (as I'll explain in a minute). As the developer, it's your responsibility to configure the Listener.php file to serve your particular circumstances and then to complete the communication chain by adding a final link in the form of a curl call to a php file in your own system to unpack and digest the transaction detail.

Before I started to look at the Stripe architecture I had naively assumed that the arrangement I have just described was the "Paypal API", which I know exists because the Paypal docs reference it and also because my Paypal account comes equipped with my personal API keys. It is only now - having had a chance to compare my IPN design with a Stripe version, that I realise that what I've got in my IPN design is not really an API at all. With Stripe, everything revolves around their API and declaration of your API keys (essentially the things that identify your Stripe account and you as its owner) is the first thing you see in your code. Nowhere in my Paypal IPN design do I declare my Paypal API keys. I now realise that I am not using "the" Paypal API at all. The real Paypal API is obviously reserved for more demanding applications than mine. It's probably not unreasonable to say that the "API" in my case is just the Listener.php and PaypalIPN.php pair.

Now, if Paypal IPN isn't using an API key, how is the security of an IPN transaction ensured? In the first instance, for example, how can you ensure that a transaction you're being notified about actually relates to your business account at all?

Here's what I found when I examined  the code closely. The "framework" code in Listener.php invites you to build an array containing all the business addresses that it is prepared to accept. Further into the code, this is checked against what Paypal is sending and an error is flagged if a match isn't located. So you might say that the "API key" of a PaypalIPN configuration is just this list of business addresses.

But an API will also usually perform more exotic checks to allow both sides of a conversation to check that they're talking to the servers they think they're talking to. How is this achieved?

Further into the Listener.php/PaypalIPN.php pair there's an elaborate arrangement where the POST parameters that have arrived in the Listener call are sent back to Paypal and checked to confirm that what was received is what was sent. This seems a bit over the top, but then I realised that the check, which is performed using a cURL command, also takes advantage of the powerful facilities provided by cURL options to check the certificates of both the peer and the host. By certificates, in this context, I mean the SSL certificates that both sides will have established on their servers to encrypt their communication (the thing that makes them https sites) and also to identify themselves.

Interestingly, now that I've got a bit of a handle on this, I think I understand a curious wrinkle that has confused me (and I suspect many others) ever since I got involved with IPN. Your Listener.php/PaypalIPN.php actually needs to be accompanied by a "cacert.pem" file placed in a folder named "cert". Paypal instructs you to store "your certificate" here. I now realise that "your certificate" in this context isn't your https certificate but rather the certificate of the Certification Authority that provided it. Paypal isn't happy to accept your site's certificate until it's seen the associated authority certificate and checked that they match up. Although cURL has a default collection of authority certificates and presumably keeps these up to date, Paypal for some reason prefers you to take some responsibility in this regard.

So much for Paypal's IPN design. When I took another run at this with Stripe, I found that, while the front-end of my system was broadly similar, the back-end code istotally different. 

For the front end, Stripe offers two styles of "payment flow" - ie the grunt business of laying out and validating the fields needed to support a card payment (card_number, expiry_date etc) on the browser/app client. There is "prebuilt" in which you take Stripe's ideas about how this is best achieved, and "custom" where you can do your own thing. In practice, it seemed to me that "custom" was nothing but trouble and didn't actually give me much effective control at all. "Prebuilt" gives you a card layout that looks very similar to the effect you get when you click on a Paypal button whereas "Custom", despite my best efforts, always looked a mess. 

In both cases, the actual business of collecting and validating the card details take place entirely within Stripe's .js libraries. But having said that, control remains with your application and there's not the same degree of independence that you see when you click a Paypal button and enter your card data into a popup that you've placed on Paypal's server. 

Whatever, you achieve all this with just a few lines of html/javascript copied from Stripe's excellent docs site at https://stripe.com/docs/payments/integration-builder. Stripe even seeds the code with the public API key that identifies your business account.

Things get much more interesting at the server-based php back-end. In the first place, the link to this is launched directly from an xmlhttp call in the client (albeit through an exotic hierarchy of "promises" launched by a javascript "fetch" call that was a novelty to me - rather nice, actually). This is a huge improvement over the Paypal arrangement, where the link between Paypal and an application Listener is outside your application's control (and in my experience, not totally reliable).

The first thing you have to do in order to build your back-end is install the appropriate version of the Stripe API library (in my case stripe-php). Stripe would prefer that you used composer here, but in my case I had to explicitly download the files and store them in my project. From here, it was largely just a case of copying boiler-plate code from the docs page quoted above. Once again, this was automatically configured with the API key required to authenticate my use of the API calls - in this case, a much more sensitive "secret" key that identified me as the owner of the account. 

It's important to note a key difference here from the Paypal arrangement. Whereas when I arrive in my backend from a Paypal payment the money has already been credited to my account, with Stripe, all that has happened thus far is authentication of card details. No payment is taken until the backend successfully completes an API call to "create a checkout session". The reason that this is done here and not in the html frontend is because this needs to be authenticated by your "secret" API key and it is only here, on a server, that you can you be confident that this is safe from inspection (and maybe then, only with some precaution as regards where you actually store it).

Whatever, to get from here to the point where I could get purchaser details for my database update, all I had to do was configure the "on success" outcome of the "checkout session" call with a link to a "grab_customer_details" php file that I wrote to make further API calls enabling me to extract an email address from Stripe's purchase record and apply it to my database.

Although it is not obvious that Stripe is using peer/host certificates to perform the security checks that are so clearly visibe in Paypal's IPN design, I'm pretty sure that these will be happening and are just tucked away within the Stripe API functions.

What else is worth saying by way of comparison?

Testing on Stripe is accomplished by changing the live version of your secret key to a test version. This has the effect of redirecting payments to the test page of your stripe account. This is very different to Paypal's arrangements where you have to change Paypal's address in your buttons to the "sandbox" site and direct the IPN to a test version of Listener.php. Performance of Paypal's sandbox can be erratic.

Another interesting observation is that Stripe suggests you should check that the price demanded for a purchase in the backend is actually what your pricing table says it should be. It appears that canny users may subvert the front end of a purchase and substitute their own version of the associated price. If this really is a problem, there's nothing you could do about it with a Paypal IPN design (since by the time you identify the problem payment has already been taken) other than initiate an interesting conversation with your purchaser.

On balance, as a developer, I think I'd have saved myself a lot of trouble if Stripe had been available to me when I wrote my system. As a purchaser, however, I'm not so sure this is the right way of looking at things. I was initially attracted to Paypal because I felt uncomfortable typing credit card details into multiple different sites. My Paypal account enables me to avoid this. As a Stripe purchaser, however, there's no purchaser Stripe account and I'm back entering my credit card details. 

But then again, things have moved on and I'm very aware that a Stripe purchase can be completed using ApplePay. So, were I purchasing with Stripe on an iPhone, I would be able to complete the purchase by just tapping a button and relying on Face Id to save me typing in a password - a very attractive arrangement indeed. Paypal doesn't offer this facility.

Finally, there's the dreaded PCI (Payment Card Industry) compliance self-assessment. Advice from Stripe is that for a system such as mine I should complete this annually. I welcome the spirit of PCI but a quick look at the sort of form I would need to complete in my case left me feeling seriously intimidated. Paypal seems to suggest that they take most of the burden for ensuring PCI compliance.

Sorry again for this length of this post, but I hope you'll agree that it's interesting and I look forward to comments.