In a nutshell, PayPal's IPN works like this:
1. A customer creates an order record on your site and is then sent to PayPal to provide payment for the goods or services they are purchasing from you;
2. Upon completion of their payment, a "silent ping" takes place from PayPal to the URL of your choice. This ping contains several pieces of information about the transaction.
3. The code at the URL you told PayPal to ping then pings PayPal back again with the exact same information posted to it; If the information you sent back is valid, PayPal will respond simply with the word "Valid"; if the information you pinged back was NOT valid, PayPal will respond with "Invalid".
4. If your response from PayPal was "Valid", you should feel good about proceeding with the update of your customer's purchase record in the database using the information contained in the original ping; If the response was "Invalid", handle that as you will.
Even though the process is fairly simple, writing the code to deal with IPN can be somewhat challenging, made mostly so by the fact that it's an "invisible ping". Troubleshooting any kind of asynchronous process (which is what IPN is) requires special approaches. So, since I already went through the process to produce what I call "ppIPN.cfc", I figured I'd go ahead and share it (if you want, you can skip directly to the download). I tried to keep it easily configurable to accommodate different apps, but at the very least it's a good set of starter code if it doesn't meet your needs as is.
ppIPN.cfc actually consists of TWO CFCs: ppIPN.cfc and ppSettings.cfc, which ppIPN extends. As you probably guessed, ppSettings is where you will put the different variables that affect how your implementation of ppIPN will work. I made it a separate CFC so that you didn't have to maintain potentially sensitive information within the one component, and so that you could put ppSettings wherever your heart desires.
Note: ppIPN assumes three things:
1. that a record for a customer already exists in your table, and the payment notification ping from PayPal is for the purpose of updating that existing record with transaction details;
2. that you are sending the ID of your customer's transaction record to PayPal by using PayPal's "CUSTOM" form field (<input type="hidden" name="custom" value="16" /> or ...&custom=16&....)
3. your cfmail tag doesn't require a mail server, username, or password to be specified (they are already set up in your cfadmin)
To use ppIPN, simply place it and the corresponding ppSettings.cfc on your webserver where they can be called via http. Adjust the 'extends' path in ppIPN.cfc if you located your ppSettings.cfc anyplace other than the same folder where ppIPN.cfc lives.
Next, edit your PayPal account to enable IPN and enter the following URL as your post back URL:
http://[your web server]/ppIPN.cfc?method=postback
Lastly, edit your ppSettings.cfc to accurately reflect your environment.
Here are the settings and a sample:
receiverEmail - the primary email address associated with your paypal account. (Though there can be several email addresses associated with your account, it is the primary email address that will be included in the IPN call from paypal, regardless of which address the payment was actually sent to)
notifyEmails - comma delimited list of email addresses you wish for notifications and status messages to go to
adminemail - the email address that will be used in the FROM of emails sent by ppIPN
ppURL - the url to use when re-posting data back to PayPal for verification
tblPayment - name of the table you're using to store transaction information
fldID - the name of the identity field for the table named above. ppINC assumes this will be an autoincrementing integer
fldFirstName - the name of the first name field in the tblPayment table. This will be the purchaser's first name.
fldLastName - the name of the last name field in the tblPayment table. This will be the purchaser's last name.
fldTransactionID - the name of the field in tblPayment in which you will be storing PayPal's transaction ID value. Should be varchar.
fldPaymentAmount - the name of the field in tblPayment in which to store the payment amount (PayPal's 'mc_gross' variable value). ppINC assumes this field is of type DOUBLE
fldPaymentDate - name of the field in tblPayment to store the transaction date. ppINC assumes type DATETIME
Here is a sample ppSettings.cfc:
instance = structnew();
instance.settings.dsn = "absolutezero";
instance.settings.receiverEmail = "email@example.com";
instance.settings.notifyEmails = "firstname.lastname@example.org,email@example.com";
instance.settings.adminemail = "firstname.lastname@example.org";
instance.settings.ppURL = "https://www.paypal.com/cgi-bin/webscr";
instance.settings.tblPayment = "REGISTRATIONS";
instance.settings.fldID = "ID";
instance.settings.fldFirstName = "FIRSTNAME";
instance.settings.fldLastName = "LASTNAME";
instance.settings.fldTransactionID = "PPTXID";
instance.settings.fldPaymentAmount = "PMTAMOUNT";
instance.settings.fldPaymentDate = "REGDATE";
Here is a sample form that can be used to test your installation of ppIPN outside of using a PayPal Sandbox (feel free to add additional PayPal fields, but currently these are the only ones ppIPN looks for). To ppIPN, this form's post appears the same as a post from PayPal:
<form action="ppIPN.cfc?method=postback" method="post">
<input type="hidden" name="payment_status" value="completed" />
<input type="hidden" name="mc_gross" value="155.00" />
<input type="hidden" name="txn_id" value="asdfsdgerweresfggzz" />
<input type="hidden" name="receiver_email" value="email@example.com" />
<input type="hidden" name="payer_email" value="firstname.lastname@example.org" />
<input type="hidden" name="custom" value="16" />
<input type="submit" value="submit" />
HOW PPIPN WORKS
1. Your customer performs a checkout on your site, creating a record in your transaction table without a payment status. Via whatever mechanism you select, they are sent to PayPal to complete the transaction, along with the ID of their record tucked within the "custom" field. VERY IMPORTANT: DO pass along the ID for the transaction record in the CUSTOM field!
2. Customer completes their transaction on PayPal. PayPal pings ppIPN with several different values.
3. ppIPN captures the incoming values, arranges them into a query string, and posts them back to PayPal.
4. PayPal validates the values ppIPN passed back and responds with either "Valid" or "Invalid".
5. If "Invalid" is received, ppIPN wraps up all the relevant values received and emails them to the addresses specified in the "notifyEmails" setting.
6. If "Valid" is received, ppIPN performs several additional checks:
is the status 'Completed'?
does this a unique transaction id that does not already exist in the db?
is the incoming receiver email value equal to that specified in the settings?
If the answer to the above is 'yes', we proceed to update the db. Otherwise, we put together a nice status email and send it out.
ADDITIONAL VALUES FROM PAYPAL
There are many variables that get returned to you via IPN calls from PayPal; ppIPN only captures and saves a few of them. To find out more about the other variables available to you, you can visit this page on PayPal's site: http://bit.ly/paypalIPN
If you wish to capture more values than what ppIPN is currently capturing, it will require some editing of the cfc. To save you some time, here are some hints on what you'll need to touch should you desire to alter what data is currently persisted:
1. Edit ppSettings.cfc and add the additional fields you want to capture (use existing settings as a guide);
2. Edit the query in ppIPN.getPaymentRecord; add your additional fields to the SELECT clause (using the variable names, not the variable values); Be SURE to alias those fields!
3. Add code in ppIPN.doValid, just below line 113, to set the values you wish to capture. In this area, you will utilize the Alias values you created in step 2. Note: All incoming PayPal variables are initially captured to the global structure "instance.incomingArgs", so any and all values that arrived in the call will be there.
Here is how to capture a new value for persistence:
<cfset existingPaymentRecord.tran_type = instance.incomingArgs.txn_type />
In this example, tran_type is the alias we gave to our transaction type field. The corresponding value we want to save coming from PayPal was captured in the instance.incomingArgs.txn_type field. txn_type is the name PayPal provided, and can be found in the IPN documentation.
4. Edit ppIPN.updatePaymentRecord, altering the query to include your new fields. You can use the existing field/value pairs as an example for adding additional ones.
Hope this saves someone a little time.
Download the ppIPN ZIP