A quirk with Stripe Customers

TL;DR

Stripe defines the description field of a Customer as:

An arbitrary string attached to the object. Often useful for displaying to users.

However, Stripe seems to use this text in their fraud prevention process. The following description fields will cause the customer create method to throw an error (at least in our case):

  • Customer for jc@gmail.com.
  • Customer for jc@gmail.com
  • Customer for abc@gmail.com.

But these will not:

  • Customer for jc@mail.com.
  • Customer for abc@mail.com.
  • Customer for jpc@gmail.com.
// This will throw an error
const { id } = await stripe.customers.create({
  description: `Customer for jc@gmail.com`,
  source: stripe_cust_token,
});

Bug discovery

While going down our Getcho QA checklist, we ran into a strange bug. This bug only showed up in prod and left few clues.

Each time I’d try to add a credit card on one of my test accounts, the Stripe form would submit normally. Great- Stripe validates the card by this point, so everything should be fine. Except the second request to our backend with the stripe token would fail. Stripe’s backend SDK returned a card_declined error. This seemed odd since we were merely creating a Stripe customer with a once-vetted payment method.

To summarize:

  • The Stripe JS SDK accepts a card and returns a Source.
  • The Stripe Node.js SDK attempts to attach this Source to the user’s Stripe Customer.
  • That fails with a card_declined error.

Diagnosing the cause

Had swathes of users been turned away from the app with this issue? Our alerts hadn’t revealed such a brick wall, but you can’t be sure. I created two new accounts and added the card that had failed to both without issue. To be certain, I dispatched an order and the charge went through.

There must have been something off about this one test account. Maybe I first made it on Stripe test mode and had since tried to add a real card?

It soon became clear that changing this user’s email address, jc@gmail.com, to a real one somehow fixed the card failure. The lone place where Stripe saw this address was in the description field:

const { id } = await stripe.customers.create({
  description: `Customer for ${user.email}`, // <= here
  source: stripe_cust_token,
});

After poking some more, I realized that attaching description fields that contained certain emails would always fail. How weird considering this field is optional!

Resolution

Since new users had to verify emails, there was no risk of a new description field containing a Stripe-rejected email. Still, I had plenty of test users with fake emails, I still made the following swap:

// Replace "@" with " at ", and each "." with " dot "
const { id } = await stripe.customers.create({
  description: `Customer for ${user.email
    .replace("@", " at ")
    .replace(/\./g, " dot ")}`,
  source: stripe_cust_token,
});

We only even used this field to make scanning customers easier.