Quick Tip: Solution to Paypal IPN Always Returning “Invalid”

Bruno Skvorc

When developing with PayPal’s IPN simulator, you might run into the situation where it keeps returning “Invalid” when verifying the message, regardless of the encoding you set or all conditions matching and being valid.

The Paypal developers team is notorious for ignoring all inquiries, and the docs are famously hard to read, so debugging these issues is incredibly hard and can cost you hours upon billable hours. I’ve even gone as far as set up a live server for testing the IPN simulator, for fear ngrok was at fault when testing locally, and even added a certificate to the endpoint to get HTTPS going – no dice. In the end, the solution was – as is usually the case – simple but obscure.

The symptom (the failure) is caused by the date field, if it contains a timezone identifier. All this, however, is caused by the fact that PHP has two different URL encoding / decoding functions: raw and non-raw.

Here’s an example.

Say we have a date in the IPN simulator going like this:

Fri Aug 19 2016 09:25:00 GMT+0100 (GMT Daylight Time)

This arrives at the listener’s end (in your PHP code) as this:


The substring GMT+0100 is problematic, because the PHP function urldecode interprets the + as a space, so it gets decoded into:

Fri Aug 19 2016 09:25:00 GMT 0100 (GMT Daylight Time)

Notice the + was lost, turned into a space character.

When this gets re-encoded for sending back to Paypal for verification, the verification fails because it’s no longer the same value in the field – the + is missing. It’s a very, very tiny detail, and incredibly hard to spot when hand-inspecting the field values, but it’s there. This is enough, as per Paypal docs, to make the verification return “INVALID”.

There are two solutions to this problem:

  1. Use rawurlencode and rawurldecode instead of their non-raw counterparts. These encode the + symbol, too, instead of turning it into a space character, and it all works then.
  2. Use a Paypal IPN Listener client which has this built in. I recently submitted a patch to this one, and it works like a charm.

Hope this little hint saved someone from lots of frustrating googling!