We make software for humans. Custom Mac, Windows, iOS and Android solutions in HyperCard, MetaCard, and RunRev LiveCode
Setting up a PayPal "Buy Now" button to work with a LiveCode CGI wasn't easy. It took days of frustration and eventually a good part of the LiveCode mailing list to work it out. The puzzle fell into place when a combination of list posts made me realize that 1) PayPal won't respond unless it gets a "200 OK" status header, and 2) Apache won't send any headers until the script exits, but PayPal needs to get the header sooner than that.
The script that follows is the solution. I couldn't have figured it out without the list's help, so I'm giving it back to the community. You can use this on your website, but please note that I retain the copyright and it can't be resold in any form.
The script could be rewritten for use with LiveCode Server. If anyone does that, let me know and I will link to it here.
That's the total communication process. Once your script knows a transaction is verified, it can do whatever it wants with the data.
If PayPal can't contact your server it will resend the purchase information repeatedly for up to 4 days. The CGI script watches for duplicates and logs them, but doesn't act on them if it's already processed the transaction. PayPal will think it hasn't reached you if it doesn't get a "Status: 200 OK" acknowledgement, so if for some reason PayPal starts re-sending the same data, you should find out why. PayPal won't get your responses if port 433 outbound is blocked so check with your ISP if you think that may be the case.
Once your tests work with the simulator, set up a sandbox "business" account and a "customer" account. Log into the business account and make a test button. Put it on an html page and test with it. The button will let you simulate the whole payment process as the "customer." Then log in as your "business" and view the transaction in the sandbox History pane. It lists all the IPN transactions, their current status, any errors received, and other helpful information.
PayPal has good documentation about using the sandbox: https://developer.paypal.com/us/cgi-bin/devscr. Once everything works in the sandbox, you can be fairly confident it will work on your real web site.
Use the "Customize Advanced Features" section at the bottom of the button generator to add an Instant Notification URL. This goes into the "advanced variables" field at the bottom of the page. The URL should be the path to your LiveCode script in the cgi-bin folder on your server, preceded by "notify_url=", like this:
If you want PayPal to return the customer to a "thank you" page after a purchase, then input the URL for that page in the "Take customers to this URL when they finish checkout" field and turn on the checkbox. This URL should be on your main web site somewhere, not in the cgi-bin folder.
I set the URL for "Take customers to this URL when they cancel their checkout" to the same purchase page they left from, but that's up to you.
When you save the button, PayPal will give you the HTML code for it. Create a purchasing page for your site and paste in the button code. This page goes somewhere on your site where visitors can access it (i.e., not in the CGI folder.)
|A web page with a PayPal button for purchasing||http://www.hyperactivesw.com/stewtest/index.html|
|A "thank you" web page that customers see after purchase||http://www.hyperactivesw.com/stewtest/thanks.html|
|A text file containing the PayPal CGI script||http://www.hyperactivesw.com/cgi-bin/paypal.mt|
|A form letter with merge variables, for customer auto-replies||http://www.hyperactivesw.com/cgi-bin/autoreply.txt|
|An empty text file for log entries||http://www.hyperactivesw.com/cgi-bin/paypalLog.txt|
|The (Linux) LiveCode/Revolution engine 3.5 or earlier||http://www.hyperactivesw.com/cgi-bin/rev|
For general info about CGI scripts and LiveCode, there's a tutorial here: http://www.hyperactivesw.com/cgitutorial/index.html. If you've never used a LiveCode CGI before, it's probably a good idea to at least get the basics there. Most of what follows assumes you know a little about it.
The sample CGI script was made for my 5-cent Beef Stew recipe. All the constants at the top must be changed to your own information. If you are working in the PayPal sandbox, some of these constants will be different than the ones for your live web site. For example, your sandbox email and merchant ID are not the same as your real merchant email and ID. The auto-fill items in the sandbox simulator won't be your items unless you manually edit them. Check all the constant values if something doesn't work, and remember to change them when you go live on your real web site.
The two script globals
gSerial are for use with HyperActive Software's Zygodact serial key generator. They don't necessarily have to be globals if you're using another registration system; they can be script locals or handler locals instead. If you aren't providing a serial key, just omit
[[gSerial]] from your email reply text. You don't need to change the CGI script.
|For Zygodact users: This script will work with the Zygodact key generator for your product. Uncomment these two lines in the startup handler:|
|Place the gen.rev stack into the cgi-bin folder with 755 permissions. That's all you need to do. You don't need the sample CGI script that ships with Zygodact.|
The CGI script simply logs the purchase data to the "paypalLog.txt" file. You may want the data to go into a database instead. If so, you'll need to work out any changes to the script; I was happy with the text log so I didn't do that. The transaction ID is returned to the main startup handler after
checkOrderData validates it, and PayPal recommends cross-checking its ID with your database. This script doesn't use it, but if you are using a database you'll probably want to store the ID for tracking or reference.
The "paypal.mt" CGI script is a text file placed in the cgi-bin folder. Be sure to set its permissions to 755 and make sure you've saved the file with unix line endings. The first line of the script must be at the very top of the text file with no blank lines before it.
You can download the CGI script as a zipped text file if you don't want to copy it from this page.
#!rev -ui global gName,gSerial -- these constants are for PayPal transactions: constant kPrice = 0.05 -- price in the Buy Now button constant kItemName = "Beef Stew Test" -- Buy Now button item name, exactly constant kItemNum = "BS100" -- Buy Now item ID constant kMerchantID = "AB1CDE23F4G5H" -- your merchant ID constant kMerchantEmail = "firstname.lastname@example.org" -- your PayPal merchant email -- these constants are for auto-reply emails: constant kCarbonAddr = "email@example.com" -- your email; the blind carbon -- and any warning messages are sent here constant kBackupAddr = "firstname.lastname@example.org" -- a backup email for warning -- messages; can be the same as the carbonAddr. If so, you'll just -- get 2 copies. constant kFromAddr = "HyperActive Software <email@example.com>" -- the "From" the -- customer sees in the auto-reply email constant kReplySubj = "Beef Stew Test purchase info" -- the subject of the -- auto-reply email on startup if $REQUEST_METHOD = "POST" then -- this is the only kind PayPal sends read from stdin until empty put it into tOrderData -- this next bit only runs if PayPal resends a duplicate notification you've -- already processed. We just log the duplicate and bail out. But if you see this, -- PayPal isn't hearing your responses and you should find out why. if tOrderData is in url ("file:paypalLog.txt") then LOG "Repeat notification" && the date && the long time && tOrderData exit startup -- this will cause Apache to throw a "server 500" error end if LOG the date && the long time && "Received:" && tOrderData -- raw PayPal data, urlEncoded put "cmd=_notify-validate&" before tOrderData -- add required response cmd if "test_ipn=1" is in tOrderData then -- we're in the sandbox put "https://www.sandbox.paypal.com/cgi-bin/webscr" into tPPAddr else -- live website put "https://www.paypal.com/cgi-bin/webscr" into tPPAddr end if put "curl --data" &"e& tOrderData "e&& tPPAddr into tPostCmd put shell(tPostCmd) into tCurlRslt put last word of tCurlRslt into tResponse -- "VERIFIED" or "INVALID"; ignore the ascii progress meter put "Status: 200 OK" &cr&cr -- without this, PP will think the server is unresponsive put "ok" -- probably superfluous but won't hurt LOG "Response:" && tResponse if tResponse = "VERIFIED" then -- data is from PayPal put checkOrderData(tOrderData) into tData -- verify the details & extract the info we need LOG "Parsed data:" && tData -- log what we extracted if tData <> "false" then -- valid data set the itemDel to "|" put item 1 of tData into gName -- already urldecoded put item 2 of tData into tEmail -- customer email address put item 3 of tData into tTransactionID -- not using it here but databases should put "Content-Type: text/plain" & cr & cr -- add a header if gName = "" then LOG "Error: name is missing." exit startup end if # -- start using stack "gen.rev" -- uncomment for Zygodact key generator; -- other systems should generate a serial key here and populate the gSerial variable with it LOG gSerial sendEmail tEmail # -- stop using stack "gen.rev" -- for Zygodact generators end if else if tResponse = "INVALID" then -- something's wrong, email me: sendEmail kCarbonAddr,"invalid" end if else -- not a POST request; just display a vague error put "Incorrect request" into buffer put "Content-Type: text/plain" & cr put "Content-Length:" && the length of buffer & cr & cr put buffer end if end startup function checkOrderData pData -- ensure that the order should be processed; -- if so, return the 3 items we need. PayPal sends the data URLEncoded. put urlDecode(pData) into pData split pData by "&" and "=" if pData["receiver_email"] <> kMerchantEmail then return false -- not for me if pData["receiver_id"] <> kMerchantID then return false -- not my PP account if pData["payment_status"] <> "Completed" then return false -- don't process pending orders if pData["txn_type"] <> "web_accept" then return false -- not a Buy Now button if (pData["first_name"] = "" and pData["last_name"] ="") and \ (pData["payer_business_name"] = "") then return false -- no name provided if pData["mc_gross"] < kPrice then return false -- underpaid; foreign funds are returned in local currency so it's okay to check the USD amount here if pData["item_name"] <> kItemName then return false -- not my item if pData["item_number"] <> kItemNum then return false -- not my item number -- data ok; get user info: put pData["first_name"] && pData["last_name"] into tName if tName = "" then put pData["payer_business_name"] into tName put pData["payer_email"] into tEmail put pData["txn_id"] into tTransactionID -- PayPal ID for this payment put "|" into tDel return tName &tDel& tEmail &tDel& tTransactionID end checkOrderData --### email handlers (thank you Andre!) on sendEmail pEmailAddr,pType if pType = "invalid" then -- notify me of spoof attempt and bcc a backup email put "Invalid purchase attempt" into tSubj put "Invalid response was received from Paypal. Check CGI log." into tMsg put kBackupAddr into tBcc else -- email to customer & blind carbon me put kReplySubj into tSubj put url ("file:autoreply.txt") into tReplyTxt put merge(tReplyTxt) into tMsg put kCarbonAddr into tBcc end if mail pEmailAddr,tSubj,tMsg,kFromAddr,tBcc end sendEmail function shellEscape pText repeat for each char tChar in "\`$" & quote -- original included "!" but it interfered with body text replace tChar with "\" & tChar in pText end repeat return pText end shellEscape function wrapQ pText -- wrap quotes around text return quote & pText & quote end wrapQ on mail pTo,pSub,pMsg,pFrom,pBcc,pCc -- send email put "Copy of mail sent:" &cr&cr & pMsg into tBCCMsg put wrapQ(shellEscape(pTo)) into pTo put wrapQ(shellEscape(pSub)) into pSub put wrapQ(shellEscape(pMsg)) into pMsg put wrapQ(shellEscape(tBCCMsg)) into tBCCMsg put wrapQ(shellEscape(pFrom)) into pFrom put wrapQ(shellEscape(pBcc)) into pBcc # put wrapQ(shellEscape(pCC)) into pCc get shell("echo -e" && pMsg && "| mail -s" && pSub && pTo && "-- -f" && pFrom) get shell("echo -e" && tBCCMsg && "| mail -s" && pSub && pBcc && "-- -f" && pFrom) -- bcc sent separately put it end mail --### Logging: on LOG pData put "paypalLog.txt" into tFile open file tFile for append write pData &cr & cr to file tFile close file tFile end LOG
The email template should contain variables for the customer name and serial key using LiveCode merge notation. The two variables
gSerial are enclosed in double square brackets, which allows LiveCode's
merge command to substitute the values of those variables when it creates the email. If you aren't using a serial key, just omit the
gSerial reference from the template. Here's a sample reply mail:
Dear [[gName]], Thanks for your purchase! You can download Jacque's Beef Stew recipe here: http://www.hyperactivesw.com/stewtest/Jacque's%20Stew.pdf If you ever feel the need to license your food, use this: Username: [[gName]] Serial: [[gSerial]] Thanks again. I think you'll like the stew. Jacqueline Landman Gay -- Jacqueline Landman Gay | firstname.lastname@example.org HyperActive Software | http://www.hyperactivesw.com
Save the auto-reply text file (with unix line endings) as "autoreply.txt", copy it to the cgi-bin folder, and set its permissions to 755.