Postal code validation for card payments


tl;dr

We recently started validating that the postal codes used for paid PythonAnywhere accounts match the ones that people’s banks have on file for the card used. This has led to some confusion, in particular because banks handle postal code validation in a complicated way – charges that fail because of this kind of error can show up in your bank app as a payment that then disappears later, or even as a charge followed by a refund. This blog post is to summarise why that is, so hopefully it will make things a bit less confusing!

The long version…

Card fraud is, sadly, a fact of life on the Internet. If you have a website that accepts payments, eventually someone will try to use a stolen card on it. If your site is online for some time, hackers might even start using you to test lists of stolen cards – that is, they don’t want to use your product in particular, they’re just trying each of the cards to find the ones that are valid, so that they can use them elsewhere.

We recently saw an uptick in the number of these “card probers” (as we call them internally) on PythonAnywhere. We have processes in place to identify them, so that we can refund all payments they get through, and report them as fraudulent to Stripe – our card processor – so that the cards in question are harder for them to use on other sites. But this takes time – time which we would much rather spend on building new features for PythonAnywhere.

Looking into the recent charges, we discovered that many of them were using the wrong postal code when testing the cards. The probers had the numbers, the expiry dates, the CVVs, but not the billing addresses. So we re-introduced something that had been disabled on our Stripe account for some time: postal code validation for payments. You may be wondering why it wasn’t enabled already, or why it might even be something that anyone would disable; this blog post is an introduction to why postal codes and card payments can be more complicated than you might think.

The first thing to understand is that postal code validation is something that was bolted on to a pre-existing mechanism for credit/debit card processing. One might naively think that when a merchant – like us – wants to charge someone’s card, they would send the network – that is, Visa, Mastercard, AmEx, or whoever – a message containing a card number, a CVV, an expiry date, the card owner’s name, the address, and the amount. Then the network would communicate that to the card owner’s bank, which would then respond with a message saying “OK, that’s done” or “the payment failed for the following reason”.

And loosely speaking, that is indeed what happens, but there’s an extra step specifically for postal code validation. For mechants using card processors like we do with Stripe, what happens (at least in our understanding) is:

  1. The card processor sends the card details and the amount to the network, which forwards it on to the bank.
  2. The bank validates all of the details and checks that the card has enough funds on it. If the card number/expiry/CVV is wrong, or there’s not enough money, they send back an error code to say that. However, if those details all check out then even if the postal code is wrong they put a “hold” on the funds. This means that nothing has been deducted from the account yet, but the money can’t be spent on anything else. They then send back a message to say that is done. This message includes whether or not the postal code validation was successful.
  3. If the postal code validation was successful, the card processor sends back a second message telling the bank to go ahead with the charge (which means that the money is actually deducted from the account). If it wasn’t, but the merchant account settings say “don’t validate postal codes” then they do the same thing. But if the merchant wants postal codes to be validated, and the validation was not successful, they send back a message saying that the hold on the funds should be reversed, and tell us that the charge failed.

If this seems like a rather roundabout process, that’s because it totally is. It was bolted on top of an existing system, and it shows.

But the biggest problem with it is that banks nowadays like to be able to provide you with an up-to-date view of your spending in their apps and on their websites – and many of them don’t handle this process well. In particular, they often show the “hold” at step 2 quickly, but then take some time to process the rejection at step 3.

When they do this, quite frequently the initial “hold” will show up as a successful charge, and then when they get around to handling the later rejection it will disappear. That is pretty confusing – but sometimes, even more confusingly, they’ll show the rejection at step 3 as if it was a refund of the earlier charge. So you’ll get a message from us saying “your postal code doesn’t match the one on file with your bank”, but in the app it will look like we charged you and then refunded it some time later.

Bank staff can also be confusing when you talk to them about this kind of issue; often if you’re dealing with their back-end support staff, they’re not familiar with the process used for postal code validation, and will only see what to them looks like us (the merchant) “rejecting” the transaction. They will not realise that the reason we (or more strictly, Stripe) are rejecting the transaction is because they – the bank – told us that the postal code provided was incorrect.

So, postal code validation is implemented as a kind of hack on top of the credit/debit card system, and the way it’s implemented and displayed to people in many banking apps is confusing. You can see why many merchants just give up and accept any postal code regardless of whether it matches the one on file with the bank. But unfortunately, due to the number of card probers and other bad actors we’ve seen recently, we’ll need to keep it in place at least for a while on PythonAnywhere. Hopefully this blog post has helped explain at least to some degree why sometimes it can lead to strange results in your bank’s app.

comments powered by Disqus