Golang : Interfacing with PayPal's IPN(Instant Payment Notification) example




A startup or any online company at minimum needs to have 3 people. One to make something, another to sell something and finally one to collect payment for having selling something. This tutorial is about collecting payment via PayPal.

PayPal has been a standard for many startups to accept payment online ( Stripe is becoming more popular these days, but they are not available worldwide yet) and this tutorial is on how to configure your Golang program to interface with PayPal's IPN(Instant Payment Notification).

Below is an example how to interface with PayPal's IPN in Golang :

 package main

 import (
  "fmt"
  "io/ioutil"
  "net/http"
  "net/url"
  "regexp"
  "strings"
 )

 // PayPal variables -- change it according to your requirements.
 // NOTE : I decided to use individual variables here so that I can include
 // explanations. It would be better and "cleaner" to put these variables
 // into array(such as url.Values{} https://golang.org/pkg/net/url/#Values ) and
 // loop the array for name and values in the form hidden fields below.

 var currency_code = "USD"
 var business = "[your email address at PayPal to receive money]" //PayPal account to receive money
 var image_url = "https://d1ohg4ss876yi2.cloudfront.net/logo35x35.png" // image on top of PayPal

 // change socketloop.com:8080 to your domain name
 // REMEMBER : localhost won't work and IPN simulator only deal with port 80 or 443
 var cancel_return = "http://[your domain]/paymentcancelreturn"
 var return_url = "http://[your domain]/paymentsuccess" // return is Golang's keyword
 var notify_url = "http://[your domain]/ipn" // <--- important for IPN to work!

 // just an example for custom field, could be username, etc. Use custom field
 // for extra verification purpose or to mark PAID status in
 // in database, etc.

 var custom = "donation"

 // See
 // https://developer.paypal.com/docs/classic/paypal-payments-standard/integration-guide/Appx_websitestandard_htmlvariables/
 // for the meaning of rm and _xclick

 var rm = "2" // rm 2 equal Return method = POST
 var cmd = "_xclick"
 var item_name = "Donation for SocketLoop"
 var quantity = "1"
 var amount = "5" // keeping it simple for this tutorial. You should accept the amount
 // from a form instead of hard coding it here.

 // uncomment to switch to real PayPal instead of sandbox
 //var paypal_url = "https://www.paypal.com/cgi-bin/webscr"
 var paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"

 func Home(w http.ResponseWriter, r *http.Request) {
  html := "<html><body><h1>You will be directed to PayPal now to pay USD " + amount + " to SocketLoop!</h1>"
  html = html + "<form action=' " + paypal_url + "' method='post'>"

  // now add the PayPal variables to be posted
  // a cleaner way to create an array and use for loop
  html = html + "<input type='hidden' name='currency_code' value='" + currency_code + "'>"
  html = html + "<input type='hidden' name='business' value='" + business + "'>"
  html = html + "<input type='hidden' name='image_url' value='" + image_url + "'>"
  html = html + "<input type='hidden' name='cancel_return' value='" + cancel_return + "'>"
  html = html + "<input type='hidden' name='notify_url' value='" + notify_url + "'>"
  html = html + "<input type='hidden' name='return' value='" + return_url + "'>" //use return instead of return_url
  html = html + "<input type='hidden' name='custom' value='" + custom + "'>"
  html = html + "<input type='hidden' name='rm' value='" + rm + "'>"
  html = html + "<input type='hidden' name='cmd' value='" + cmd + "'>"
  html = html + "<input type='hidden' name='item_name' value='" + item_name + "'>"
  html = html + "<input type='hidden' name='quantity' value='" + quantity + "'>"
  html = html + "<input type='hidden' name='amount' value='" + amount + "'>"

  html = html + " <input type='submit' value='Proceed to PayPal'></form></body></html>"

  w.Write([]byte(fmt.Sprintf(html)))

 }

 func PaymentSuccess(w http.ResponseWriter, r *http.Request) {
  // This is where you would probably want to thank the user for their order
  // or what have you.  The order information at this point is in POST
  // variables.  However, you don't want to "process" the order until you
  // get validation from the IPN.  That's where you would have the code to
  // email an admin, update the database with payment status, activate a
  // membership, etc.

  html := "<html><body><h1>Thank you! Payment accepted!</h1></body></html>"
  w.Write([]byte(fmt.Sprintf(html)))
 }

 func PaymentCancelReturn(w http.ResponseWriter, r *http.Request) {
  html := "<html><body><h1>Oh ok. Payment cancelled!</h1></body></html>"
  w.Write([]byte(fmt.Sprintf(html)))
 }

 func IPN(w http.ResponseWriter, r *http.Request) {
  // Payment has been received and IPN is verified.  This is where you
  // update your database to activate or process the order, or setup
  // the database with the user's order details, email an administrator,
  // etc. You can access a slew of information via the IPN data from r.Form

  // Check the paypal documentation for specifics on what information
  // is available in the IPN POST variables.  Basically, all the POST vars
  // which paypal sends, which we send back for validation.

  // For this tutorial, we'll just print out all the IPN data.

  fmt.Println("IPN received from PayPal")

  err := r.ParseForm() // need this to get PayPal's HTTP POST of IPN data

  if err != nil {
 fmt.Println(err)
 return
  }

  if r.Method == "POST" {

 var postStr string = paypal_url + "&cmd=_notify-validate&"

 for k, v := range r.Form {
 fmt.Println("key :", k)
 fmt.Println("value :", strings.Join(v, ""))

 // NOTE : Store the IPN data k,v into a slice. It will be useful for database entry later.

 postStr = postStr + k + "=" + url.QueryEscape(strings.Join(v, "")) + "&"
 }

 // To verify the message from PayPal, we must send
 // back the contents in the exact order they were received and precede it with
 // the command _notify-validate

 // PayPal will then send one single-word message, either VERIFIED,
 // if the message is valid, or INVALID if the messages is not valid.

 // See more at
 // https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/

 // post data back to PayPal
 client := &http.Client{}
 req, err := http.NewRequest("POST", postStr, nil)

 if err != nil {
 fmt.Println(err)
 return
 }

 req.Header.Add("Content-Type: ", "application/x-www-form-urlencoded")

 // fmt.Println(req)

 resp, err := client.Do(req)

 if err != nil {
 fmt.Println(err)
 return
 }

 fmt.Println("Response : ")
 fmt.Println(resp)
 fmt.Println("Status :")
 fmt.Println(resp.Status)
 

 // convert response to string
 respStr, _ := ioutil.ReadAll(resp.Body)

 //fmt.Println("Response String : ", string(respStr))

 verified, err := regexp.MatchString("VERIFIED", string(respStr))

 if err != nil {
 fmt.Println(err)
 return
 }

 if verified {
 fmt.Println("IPN verified")
 fmt.Println("TODO : Email receipt, increase credit, etc")
 } else {
 fmt.Println("IPN validation failed!")
 fmt.Println("Do not send the stuff out yet!")
 }

  }

 }

 func main() {
  // http.Handler
  mux := http.NewServeMux()
  mux.HandleFunc("/", Home)
  mux.HandleFunc("/paymentcancelreturn", PaymentCancelReturn) // remember, case sensitive
  mux.HandleFunc("/paymentsuccess", PaymentSuccess) // remember, case sensitive
  mux.HandleFunc("/ipn", IPN)

  http.ListenAndServe(":8080", mux)
 }

Change the domain name to yours and run the code.

Point your browser to the main URL will show you this page :

home pay pal

and after clicking the button :

after click

follow by after payment :

after payment

finally, after the IPN message was posted back to PayPal and verified :

verified IPN

r.Form contains useful data from IPN. Use the data for extra verification purpose, such as making sure the amount is the same before posting to PayPal. Email or username ( via custom field ) is the same before posting to PayPal. These extra security step are meant to make sure that no data changed(spoofing) during postings.

References :

https://github.com/paypal/ipn-code-samples/blob/master/paypal_ipn.pl

https://github.com/asadovsky/tadue/blob/master/app/paypal.go

https://www.socketloop.com/tutorials/golang-parsing-or-breaking-down-url





By Adam Ng

IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.


Advertisement