Paypal IPN: Instant Payment Notification
Instant Payment Notification (IPN) is a message service that notifies you of events related to PayPal transactions. You can use IPN messages to automate back-office and administrative functions, such as fulfilling orders, tracking customers or providing status and other transaction-related information.
The IPN service triggers a notification when an event occurs that pertains to a transaction. Typically, these events represent various kinds of payments; however, the events may also represent authorizations, Fraud Management Filter actions and other actions, such as refunds, disputes and chargebacks. The notifications are sent to a listener page on your server which receives the messages and performs backend processing based on the message content.
Prerequisites
You will need a PayPal developer account (aka sandbox) to test the integration before going live. If you don't have an account at PayPal for Developers, create it from here: PayPal for Developers.
Checkout technical flow
To better understand how IPN and the checkout process work take a look to the following diagram.
You add the payment button to your web page.
Your buyer clicks the button
The button calls the PayPal orders API to set up a transaction.
The button launches the PayPal Checkout experience.
The buyer approves the payment.
The button calls the PayPal orders API to finalize the transaction.
You show a confirmation message to your buyer.
Adding the payment button
To accept payments on your website, you need to add the PayPal JavaScript SDK code to your checkout page. In the past, raw HTML forms were accepted but nowadays it's considered legacy and deprecated.
<!-- TODO Replace [CLIENT_ID] with your own app client ID -->
<script src="https://www.paypal.com/sdk/js?client-id=[CLIENT_ID]"></script>
The client ID for testing (sandbox) and production (live) can be generated in your developer account within My Apps & Credentials section available in the sidebar menu. Default application (if present) can be used or a new one generated.
Together with client ID a a secret key is generated.
Once the SDK is loaded the button will be automatically rendered with the following code:
<div id="paypal-button-container"></div>
<script type="text/javascript">
paypal.Buttons({
// Order is created on the server and the order id is returned
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [{
description: 'HiBit: item description',
amount: {
currency_code: 'USD',
value: 2.50
},
custom: 'optional_custom_field_value'
}]
});
},
onApprove: function(data, actions) {
return actions.order.capture().then(function(orderData) {
// Full available details
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
// Or go to another URL: actions.redirect('thank_you.html');
//actions.redirect('thank_you.html');
});
},
onError: function(err) {
console.log(err);
}
}).render('#paypal-button-container');
</script>
Configure purchase_units attribute to correctly show product description, price and currency. A custom field can be attached to the order that will be available later in the IPN handler.
Preparing IPN listener
You receive and process IPN messages with a listener (sometimes called a handler). This listener is basically an endpoint that you create on your server that is always active and had code that allows it to accept and verify IPN messages sent from PayPal, and then invoke necessary backend services, based on the information from the IPN message. We've prepared a sample code using PHP language to represent an integration example.
Message example
The IPN message posted to the listener will be similar to the following one:
"payment_type": "echeck",
"payment_date": "23:31:11 Aug 08, 2022 PDT",
"payment_status": "Completed",
"address_status": "confirmed",
"payer_status": "verified",
"first_name": "John",
"last_name": "Smith",
"payer_email": "[email protected]",
"payer_id": "TESTBUYERID01",
"address_name": "John Smith",
"address_country": "United States",
"address_country_code": "US",
"address_zip": "95131",
"address_state": "CA",
"address_city": "San Jose",
"address_street": "123 any street",
"business": "[email protected]",
"receiver_email": "[email protected]",
"receiver_id": "[email protected]",
"residence_country": "US",
"item_name": "something",
"item_number": "AK-1234",
"quantity": "1",
"shipping": "3.04",
"tax": "2.02",
"mc_currency": "USD",
"mc_fee": "0.44",
"mc_gross": "12.34",
"mc_gross_1": "12.34",
"txn_type": "web_accept",
"txn_id": "255514245",
"notify_version": "2.1",
"parent_txn_id": "SOMEPRIORTXNID003",
"reason_code": "other",
"custom": "xyz123",
"invoice": "abc1234",
"test_ipn": "1",
"verify_sign": "ApV.TKg645AF013D4.ZnodnMmCm-AOwBQeqXKIHNERveq5.w0zP3vS7s"
All keys and possible values can be reviewed in the official PayPal documentation: IPN and PDT variables.
The listener
IPN is an asynchronous message service, meaning that IPNs are not synchronized with actions on your website and the listener should be implemented as an autonomous process. Listening for an IPN message does not increase the time required to complete a transaction on your website.
Check out our example of PHP implementation for PayPal IPN handler. It contains missing TODOs that represent your application logic.
use Paypal\\\\Php\\\\Exception\\\\IpnException;
use Paypal\\\\Php\\\\ValueObject\\\\PaypalIpn;
use Paypal\\\\Php\\\\Repository\\\\PaypalRepository;
define('PAYPAL_IPN_HANDLER', 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr'); //Sandbox
try {
// TODO validate receiver_email value match your account
// TODO validate mc_currency match the currency you expect
// TODO validate mc_gross match the product/service price
// TODO validate txn_id was not previously processed
// TODO validate payment_status
$paypalRepository = new PaypalRepository(PAYPAL_IPN_HANDLER);
// Verify that we have a correct and valid IPN message
$paypalRepository->verify(new PaypalIpn($_POST));
// TODO log payment information and details in success log
// TODO process related products and/or services
} catch (IpnException $e) {
// TODO log exception and error to review manually
}
//TODO return HTTP 200 code with empty content
Note: we strongly recommend defining handler URL as environment variable instead of hardcoding it.
In case of some error, IpnException will be thrown. You should log it and manually review the reason of the fail. The IPN message service includes a retry mechanism that re-sends a message at various intervals until your listener acknowledges receipt, and, depending on the error, you may want to return a success response code (HTTP 200) to avoid retries of the same event/message.
The repository
Our repository has one and unique goal of verifying the incoming message, so we pass all the input data to call PayPal servers and verify the message. PayPalIpn class will take the responsibility of transforming the message to a string, adding necessary keys and removing unneeded encoding/decoding characters.
public function verify(PaypalIpn $paypalIpn): void
{
$ch = curl_init($this->handlerUrl);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'User-Agent: HiBit-IPN',
'Connection: Close',
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $paypalIpn->value());
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch); // Holds the response string from the PayPal's IPN.
$curlCode = curl_errno($ch); // ErrorID
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (empty($response)) {
throw new IpnException(sprintf('cURL error: %d', $curlCode));
}
if ($httpCode !== 200) {
throw new IpnException(sprintf('IPN responded with HTTP code: %d', $httpCode));
}
// Check if PayPal verifies the IPN data, and if so, return true.
if ($response === 'INVALID') {
throw new IpnException('Paypal response is invalid');
}
}
This method does not return values but throw IpnException in case of some error.
Conclusion
You can find advanced examples and more implementation details in our official GitHub repository. Do not forget to configure the IPN listener endpoint from your PayPal account settings and make sure to fully test your integration before going live.
Credits
Official GitHub: https://github.com/hibit-dev/paypal-ipn
0 Comments