HyperActive Software

Home What's New Who We Are What We Do Solutions Resources

We make software for humans. Custom Mac, Windows, iOS and Android solutions in HyperCard, MetaCard, and RunRev LiveCode

Resources...

Using PayPal with LiveCode CGIs

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.

How PayPal implements the Instant Notification System

When a customer purchases from your web site, PayPal will send the purchase information to the URL of a script you provide. Your script must return the data exactly as received, with a single parameter added at the front. PayPal compares the data you return with the data it sent, and if they match, it sends a single word: "VERIFIED". If there is no match (i.e., someone is trying to scam your purchase page) it will send back "INVALID".

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.

Use the sandbox

PayPal has a sandbox that you should test in. You'll need to set up a sandbox account and use the IPN simulator to make sure the script is working. That allows you to test without spending any real money, and gives you some feedback about the transactions.

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.

Configure a PayPal "Buy Now" button

Go to "My Saved Buttons" in the Merchant Services pane in your PayPal sandbox account, and use PayPal's button generator to create a Buy Now button. Note the exact item name and ID you use, as you'll need those in the CGI script. You'll also need your merchant ID, which is listed in your account profile.

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:

notify_url=http://www.hyperactivesw.com/cgi-bin/paypal.mt

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.)

Overview of required server files

Here's a summary of the files that must be on your server. They're explained below.

Description Example URL
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

Create the cgi script and files

The cgi-bin folder will contain your LiveCode script (traditionally with the extension ".mt" but it can be ".cgi" too.) The folder will also contain the boilerplate reply email for the customer and a log file for tracking purchase data from PayPal. It also needs a copy of the LiveCode engine suitable for the server OS (usually Linux.) The engine is named "rev" here for historical reasons but you can rename it if you like. Change the first line of the CGI script to match if you do that.

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 gName and 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:
start using stack "gen.rev"
...
stop using stack "gen.rev"
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.

LiveCode CGI script "paypal.mt"


#!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 = "salesteam@hyperactivesw.com" -- your PayPal merchant email

-- these constants are for auto-reply emails:
constant kCarbonAddr = "jlg@hyperactivesw.com" -- your email; the blind carbon 
              -- and any warning messages are sent here
constant kBackupAddr = "private@hyperactivesw.com" -- 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 <salesteam@hyperactivesw.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" &&quote& tOrderData &quote&& 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

Create the email reply template

The reply template contains the text of the email your customers will receive. You'll also get a copy of it.

The email template should contain variables for the customer name and serial key using LiveCode merge notation. The two variables gName and 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         |     dishwasher@hyperactivesw.com
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.

Add a log file

This is easy. Just make an empty text file named "paypalLog.txt", copy it to the cgi-bin folder, and set its permissions to 766. Transactions will be recorded here. If you're using a database instead, you probably won't need this file unless you want to keep a raw record of the transactions. Remember to change the "LOG" handler if you omit this file, otherwise the script will error.

Test it

That's the setup. Try the script in PayPal's sandbox simulator first. If that works, try a sandbox button on a web page. And if that works, change the values of the script constants and go live!