Idempotent Stripe Requests

Since the beginning, Stripe has had an easy-to-use API. This didn't make it safe, though. Every time you make a POST request to Stripe, you're changing something. Sometimes this is innocuous, but often it's vitally important that this thing happen once and only once. For example, creating new charges, subscriptions, and customers should ideally only happen once. This is now possible with a new Stripe feature named Idempotency.

Stripe recently added a new piece of functionality that helps you to ensure that, even if you mess up and send the same request twice, you're not going to cause problems. It's called the Idempotency Key, and it works like this:

  • When you make a POST request you can include a header named Idempotency-Key. The value is completely up to you.
  • For a given POST request, Stripe will only process the request once per Idempotency-Key.
  • If Stripe successfully processes the request (meaning, it succeeded or it failed due to some error within your control), they'll cache their response for 24 hours. Any further requests made with the same Idempotency-Key will return the same response.

This is an incredbly powerful tool that helps you build robust payment systems. Let's dig into an example.

Making Idempotent Charges

Let's say you sell products, and you've designed your Rails app to be robust in the face of failure (e.g. by following the recommendations in Mastering Modern Payments and Design for Failure). You're processing requests in the background, like this:

class StripeCharger
  include Sidekiq::Worker

  def perform(event)
    ActiveRecord::Base.connection_pool.with_connection do
      token =  event[:token]
      txn = Transaction.find(event[:transaction_id])

      begin
        charge = Stripe::Charge.create(
          amount: txn.amount,
          currency: "usd",
          card: token,
          description: txn.email
        )
        txn.state = 'complete'
        txn.stripe_id = charge.id
        txn.save!
      rescue Stripe::StripeError => e
        txn.state = 'failed'
        txn.error = e.json_body
        txn.save!
      end
    end
  end
end

Your call to Stripe::Charge.create is happening inside an exception handler, and if it fails because something happened within Stripe you save off the error and finish the job.

If an exception happens that is not a Stripe::StripeError, your job will get retried. It will, of course, immediately fail, because you've already consumed the single-use token.

Now, with idempotent requests, you can do this instead:

class StripeCharger
  include Sidekiq::Worker

  def perform(event, unique_job_key)
    ActiveRecord::Base.connection_pool.with_connection do
      token =  event[:token]
      txn = Transaction.find(event[:transaction_id])

      begin
        charge = Stripe::Charge.create({
          amount: txn.amount,
          currency: "usd",
          card: token,
          description: txn.email
        }, { idempotency_key: unique_job_key }) # <-- new stuff
        txn.state = 'complete'
        txn.stripe_id = charge.id
        txn.save!
      rescue Stripe::StripeError => e
        txn.state = 'failed'
        txn.error = e.json_body
        txn.save!
      end
    end
  end
end

Notice how we're now passing in a key and then passing it to Stripe::Charge.create. We'll end up queuing the job like this:

StripeCharger.perform_async(event_options, SecureRandom.uuid)

Every time this job runs over the next 24 hours, if it hits Stripe it'll return the same response. You don't have to worry about creating multiple charges, and you also don't have to worry about getting errors about trying to use a token twice.

Every POST can be Idempotent

You can add an idempotency key to any POST request you make to Stripe. This isn't just for charges with tokens, although it's definitely useful.

For example, idempotency comes in very handy when you're creating new charges or adding subscriptions for existing customers. In that situation you don't have the single-use guarantee that comes with a token generated by Stripe.js.

To use it this way, you could pass your idempotency key through the user's browser, through your payment form and back to your controller, which would insulate you from accidental double clicks along with network hiccups.

How to get it

To get this feature you'll need to upgrade to at least v1.16.1 of the Stripe ruby gem. You can read Stripe's docs about Idempotent Requests right here.

Mastering Modern Payments

Build a Better Payment System

Get a free five part email course all about Stripe and Rails, including the first three chapters of Mastering Modern Payments.


No spam. Unsubscribe at any time.
Pete Keen Portrait Pete Keen has been professional software developer for a decade, building payment systems and other software for companies large and small. He blogs here and at petekeen.net.