Selling Downloads with Stripe and Laravel

Share this article

Digital goods are an increasingly valuable commodity. Whether you’re a designer selling templates or font files, a developer charging for packages of code or a musician selling MP3s, selling digital goods online is often far easier than physical goods – with much lower production costs and no delivery charges.

In this article I’ll show how you can implement a simple store selling digital goods using PHP along with Stripe, a payment provider who aims to make it easier than ever to take online payments since you don’t need to set up special merchant accounts or deal with complex payment gateways.

Before you Begin

You’ll first need to set up an account with Stripe. Please note that the service is currently only available in the US, UK, Ireland and Canada. You may wish to check the blog or follow them on Twitter to be kept up to date in regards to avalability in other countries.

Setting up an account for testing takes no time at all, and doesn’t require any complex financial questionnaires or information. You’ll need to take a note of your test API key and publishable key for later.

Setting Up

For this example application I’m going to use Laravel. It’ll take care of some of the bread-and-butter stuff such as routing, templating and initiating the downloads, but the example shouldn’t be too difficult to adapt for other frameworks (or indeed, no framework at all). Note that you can clone the entire example from Github.

Get started by installing Laravel via Composer:

composer create-project laravel/laravel stripe-downloads --prefer-dist

Include the following in the require section of your composer.json file, and run composer update:

"abodeo/laravel-stripe": "dev-master"

This provides a simple wrapper to the Stripe SDK, allowing it to be used from within Laravel without having to worry about require‘ing the appropriate files.

Publish the configuration file using:

php artisan config:publish abodeo/laravel-stripe

Then enter your API key and publishable key in app/config/packages/abodeo/laravel-stripe/stripe.php

Finally add the package to your list of service providers (app/config/app.php):

'Abodeo\LaravelStripe\LaravelStripeServiceProvider',

Setting up the Database

Configure and create your database, then run the following to create a migration:

php artisan migrate:make create_downloads_table

Here’s the relevant section (of the up() function), to create a table for the downloads we’re going to sell:

Schema::create('downloads', function($table)
{
    $table->increments('id')->unsigned();        
    $table->string('name', 255);      
    $table->string('filepath', 255);      
    $table->integer('price');            
    $table->timestamps();
});    

Note that price is an integer, because internally we’re only going to deal with cents / pence. Filepath will refer to the location of the file relative to the app/storage directory.

The corresponding model is very simple:

// app/models/Download.php
class Download extends Eloquent {
    protected $fillable = array('name', 'filepath', 'price');
}

Finally, let’s seed the database with some sample downloads:

class DownloadsSeeder extends Seeder {

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $data = array(
            array(
                'name'            =>    'Sample download 1',
                'filepath'    =>    'downloads/one.zip',
                'price'            =>    500,
            ),
            array(
                'name'            =>    'Sample download 2',
                'filepath'    =>    'downloads/two.zip',
                'price'            =>    1000,
            ),
        );

        foreach ($data as $properties) {

            $download = new Download($properties);
            $download->save();            

        }
    }

}    

A Simple Homepage

Let’s create a simple homepage which allows people to select which download they’d like to purchase:

Route::get('/', function()
{
    $downloads = Download::get();    
    return View::make('index', array('downloads' => $downloads));
});

And the view:

// app/views/index.blade.php
<h1>Select your download:</h1>

<table class="table table-bordered">
@foreach ($downloads as $download)
    <tr>
        <td>{{ $download->name }}</td>
        <td>&pound;{{ round($download->price/100) }}</td>
        <td><a href="/buy/{{ $download->id }}" class="btn btn-primary">Buy</a></td>
    </tr>
@endforeach
</table>

Taking Payment

Stripe provides two methods for taking card details. The first is a simple button, generated using Javascript which launches a payment form in a popup like so:

image

The second and more complex method allows to create the form yourself. That’s the method I’m going to use in this example. Here’s the route:

Route::get('/buy/{id}', function($id)
{
    $download = Download::find($id);    
    return View::make('buy', array('download' => $download));
});

And the view:

// app/views/buy.blade.php
<form action="" method="POST" id="payment-form" role="form">

  <div class="payment-errors alert alert-danger" style="display:none;"></div>

  <input type="hidden" name="did" value="{{ $download->id }}" />

  <div class="form-group">
    <label>
      <span>Card Number</span>
      <input type="text" size="20" data-stripe="number" class="form-control input-lg" />
    </label>
  </div>

  <div class="form-group">
    <label>
      <span>CVC</span>
      <input type="text" size="4" data-stripe="cvc" class="form-control input-lg" />
    </label>
  </div>

  <div class="form-group">  
    <label>
      <span>Expires</span>      
    </label>
    <div class="row">
      <div class="col-lg-1 col-md-1 col-sm-2 col-xs-3">
        <input type="text" size="2" data-stripe="exp-month" class="input-lg" placeholder="MM" />
      </div>  
      <div class="col-lg-1 col-md-1 col-sm-2 col-xs-3">
        <input type="text" size="4" data-stripe="exp-year" class="input-lg" placeholder="YYYY" />
      </div>
    </div>
  </div>

    <div class="form-group">
      <button type="submit" class="btn btn-primary btn-lg">Submit Payment</button>
  </div>
</form>

The eagle-eyed among you will notice that the card-related inputs are missing one crucial attribute – name. Without this, no data can possibly be passed to our server when the form gets submitted. How can this be?

Actually, this is deliberate. We don’t want any credit card data being sent to our application.

What we’re going to do is intercept the submission of this form using Javascript. The card information will be extracted from the form – notice the data-stripe attributes – and securely sent to the Stripe servers along with our publishable key. Stripe are then responsible for validating those details and all being well, return to us a special token. Once we have received that token, we can inject it into the form and POST the results as normal – minus the card details.

Back on the server, we can then use this token in conjunction with our private (API) key to actually charge the user’s card. Should that code get intercepted up to this point, it’s useless without our private key so there’s very little a hacker could do with it.

We start by initialising the Stripe Javascript:

Stripe.setPublishableKey('@stripeKey');

In the code above, @stripeKey is a special Blade extension which outputs the publishable key. If you’re not using the blade templating library, you may wish to do something like this instead:

Stripe.setPublishableKey('<?php print Config::get('stripe.publishableKey') ?>');

Next up, here’s the submit handler for our form:

$('#payment-form').submit(function(e) {
  var $form = $(this);

  $form.find('.payment-errors').hide();

  $form.find('button').prop('disabled', true);

  Stripe.createToken($form, stripeResponseHandler);

  return false;
});

The function to handle the response from Stripe:

    function stripeResponseHandler(status, response) {
      var $form = $('#payment-form');

      if (response.error) {

        $form.find('.payment-errors').text(response.error.message).show();
        $form.find('button').prop('disabled', false);
      } else {

        var token = response.id;

        $form.append($('<input type="hidden" name="stripeToken" />').val(token));

        $form.get(0).submit();
      }
  };

Note how the token gets injected into the form by dynamically creating a new hidden form element with the name stripeToken. This is the only payment-related data that gets submitted.

Should you examine the response from the Stripe server, you’ll notice that in addition to the token – the id property – there is a card dictionary object. Obviously this does not contain the card number or CVC, but it does include the type – Visa, Mastercard etc – and the last 4 digits of the card number. This is useful for generating invoices or receipts; it’s quite common to include this information to indicate to the customer what card they used to pay.

Let’s build the POST form handler:

    Route::post('/buy/{id}', function($id)
    {
        // Set the API key    
        Stripe::setApiKey(Config::get('laravel-stripe::stripe.api_key'));

        $download = Download::find($id);

        // Get the credit card details submitted by the form
        $token = Input::get('stripeToken');

        // Charge the card
        try {
            $charge = Stripe_Charge::create(array(
                "amount" => $download->price,
                "currency" => "gbp",
                "card" => $token,
                "description" => 'Order: ' . $download->name)
            );

            // If we get this far, we've charged the user successfully

        Session::put('purchased_download_id', $download->id);
        return Redirect::to('confirmed');

        } catch(Stripe_CardError $e) {
          // Payment failed
        return Redirect::to('buy/'.$id)->with('message', 'Your payment has failed.');        
        }
    }

Let’s go through this.

First we’re setting the API key for Stripe. When we handled the form client-side we were okay with exposing the publishable key, because the token is useless without the private API key.

Next we get the download being purchased from its ID, so we can get the name (used in the transaction reference) and its price (in cents / pence etc).

The value of $token is what we got from Stripe in the Javascript above, which we then injected into the form.

Next we use Stripe_Charge::create to actually charge the card. The $currency setting isn’t required, but it will default to USD even if you set up an account outside of the US.

If the payment succeeds, we put the ID of the purchased item in the session. We’ll use this to control access in a moment.

If the payment fails – usually because the bank declined the transaction – a Stripe_CardError exception gets thrown.

You can test the payment process using the dummy card number 4242-4242-4242-4242, along with any three-digit CVC and any expiry date – provided it’s in the future.

The confirmation route is simple:

Route::get('/confirmed', function()
{
    $download = Download::find(Session::get('purchased_download_id'));
    return View::make('confirmed', array('download' => $download));
});

The view, which allows the buyer to download their file by clicking the download button:

// app/views/confirmed.blade.php
<h1>Your Order has been Confirmed</h1>

<p>You can now download your file using the button below:</p>

<p><a href="/download/{{ $download->id }}" class="btn btn-lg btn-primary">Download</a></p>

Delivering the File

Finally, we need to implement the download link. We don’t want to simply link to a public file, since we’re charging for access. Instead, we’ll take the file from the application’s storage directory – which isn’t web accessible – and deliver if, and only if, the current user has successfully paid for it.

Route::get('/download/{id}', function($id)
{
    $download = Download::find($id);        
    if ((Session::has('purchased_download_id') && (Session::get('purchased_download_id') == $id))) {
        Session::forget('purchased_download_id');
        return Response::download(storage_path().'/'.$download->filepath);    
    } else {
        App::abort(401, 'Access denied');
    }
});

This link can only be used once; in practice you’d probably want to make it possible to download a file again at a later date, but I’ll leave you to come up with ways of doing that.

Summary

In this article I’ve shown how simple it is to take payments using Stripe, without having to worry about merchant accounts, payment gateways or storing sensitive card information. We’ve implemented a working, albeit simple purchasing process for digital downloads. Hopefully you’ll have seen enough to get started implementing it in your projects.

Frequently Asked Questions (FAQs) on Selling Downloads with Stripe and Laravel

How Can I Integrate Stripe with Laravel for Selling Downloads?

Integrating Stripe with Laravel for selling downloads involves a few steps. First, you need to install the Stripe PHP library and Laravel Cashier via Composer. Then, you need to configure your environment variables with your Stripe API keys. After that, you can set up your database and models to handle Stripe’s billing. Finally, you can create routes and controllers to handle the checkout process and file downloads. Remember to test your integration thoroughly before going live.

What Are the Benefits of Using Stripe with Laravel for Selling Downloads?

Stripe and Laravel together provide a robust and secure solution for selling downloads. Stripe handles all the complexities of payment processing, including security, fraud detection, and compliance with international regulations. Laravel, on the other hand, provides a clean and elegant framework for building web applications, with built-in support for Stripe through the Cashier package. This combination allows you to focus on your core business while ensuring a smooth and secure checkout experience for your customers.

How Can I Handle Recurring Payments with Stripe and Laravel?

Laravel Cashier provides an expressive, fluent interface to Stripe’s subscription billing services. You can create plans in Stripe, and then use Cashier to subscribe your users to these plans with just a few lines of code. Cashier handles almost all of the boilerplate subscription billing code you are dreading writing, including handling coupons, swapping subscription, subscription “quantities”, cancellation grace periods, and even generating invoice PDFs.

How Can I Handle Refunds with Stripe and Laravel?

Refunds in Stripe are represented by refund objects. You can create a refund through the Stripe Dashboard or via the Stripe API. Laravel Cashier does not directly provide a method for handling refunds, but you can use the Stripe PHP library to interact with the Stripe API and create refunds. Remember to handle refunds in your application logic as well, for example by revoking access to the downloaded files.

How Can I Secure My Stripe and Laravel Integration?

Security is a critical aspect of any payment processing system. Stripe provides several features to help secure your transactions, including HTTPS for all data transmission, encryption of sensitive data, and PCI compliance. Laravel also provides several security features, including protection against cross-site scripting (XSS), cross-site request forgery (CSRF), and SQL injection. Additionally, you should always keep your Stripe API keys secure and never expose them in client-side code.

How Can I Handle Errors and Exceptions in Stripe and Laravel?

Both Stripe and Laravel provide mechanisms for handling errors and exceptions. Stripe returns HTTP status codes and error codes to indicate what went wrong with a request. Laravel provides exception handling through its Handler class, which allows you to report and render exceptions in a user-friendly manner. You should always handle errors and exceptions in your code to ensure a smooth user experience and to prevent sensitive information from being exposed.

How Can I Test My Stripe and Laravel Integration?

Testing is a crucial part of any development process. Stripe provides a set of test API keys that you can use to simulate transactions without actually charging any money. Laravel provides a powerful testing suite that allows you to write unit tests and feature tests for your application. You should always write tests for your code to ensure that it works as expected and to catch any bugs or issues early on.

How Can I Monitor My Stripe and Laravel Integration?

Monitoring your Stripe and Laravel integration can help you identify and resolve issues quickly. Stripe provides a dashboard that shows your payment activity and performance. Laravel provides several tools for monitoring your application, including the Telescope debugging assistant and the Horizon queue dashboard. You can also use third-party services like Bugsnag or Sentry for error tracking and monitoring.

How Can I Optimize My Stripe and Laravel Integration?

Optimizing your Stripe and Laravel integration can improve your application’s performance and user experience. You can optimize your Stripe integration by using webhooks to handle events, batching requests to reduce API calls, and using the Stripe.js library to offload payment processing to the client side. You can optimize your Laravel application by using eager loading to reduce database queries, caching responses to speed up requests, and queueing long-running tasks to improve response times.

How Can I Update My Stripe and Laravel Integration?

Updating your Stripe and Laravel integration involves updating the Stripe PHP library and Laravel Cashier to the latest versions. You should always keep your dependencies up to date to benefit from the latest features and security fixes. Remember to test your application thoroughly after updating to ensure that everything still works as expected.

Lukas WhiteLukas White
View Author

Lukas is a freelance web and mobile developer based in Manchester in the North of England. He's been developing in PHP since moving away from those early days in web development of using all manner of tools such as Java Server Pages, classic ASP and XML data islands, along with JavaScript - back when it really was JavaScript and Netscape ruled the roost. When he's not developing websites and mobile applications and complaining that this was all fields, Lukas likes to cook all manner of World foods.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form