DEV Community

Cover image for Golang Payment Gateway Integration with Rapyd
Kyle Pollock for Rapyd

Posted on • Edited on • Originally published at community.rapyd.net

Golang Payment Gateway Integration with Rapyd

By Christoph Berger

Integrating a purchase flow into a web application can be complicated. Whether you want to sell a single item or service online or build a full-fledged web shop, you need a payment gateway, which enables the secure processing of electronic payments between a merchant and a payment processor (ie a bank or a credit card company). A payment gateway sends the transaction information from the customer to the acquiring financial institute for authorization and then sends the response back to the merchant.

Payment gateways like Rapyd provide additional benefits, including increased security for sensitive financial information, improved reliability of payment processing, ease of integration, and advanced fraud detection and prevention capabilities. Rapyd is a certified PCI DSS Level 1 service provider.

In this article, you'll focus on a particular benefit: the ease of integration. You'll learn how to build payment processing into your app in a few easy steps using the Rapyd Collect API.

What Is the Rapyd Collect API?

If you're looking for a positive buying experience for your customers with endpoints for payments, products, subscriptions, coupons and discounts, orders, and invoices, the Rapyd Collect API has you covered. This API takes care of collecting payments from customers into Rapyd Wallet, a central component for receiving, storing, and sending money. The Rapyd Collect API provides endpoints for payments, products, subscriptions, coupons and discounts, orders, invoices, and of course, endpoints for easy checkout process integration, which you'll learn more about in the following sections.

For the checkout flow you'll be working on here, you need only access the checkout page endpoints. The Checkout Page API provides two ways of integrating the checkout:

  1. A hosted checkout page integration
  2. A checkout toolkit integration

The hosted checkout page integration is an all-in-one solution. This means Rapyd generates the checkout page for you, and you need only supply the amount, the customer's country, the currency, and other optional data (if desired), then Rapyd manages the rest of the checkout flow. The hosted checkout page is not only convenient but also available for clients who have no PCI certification.

The checkout toolkit integration is a more advanced alternative. It's useful if you want to have more control over your layout and user experience by embedding checkout fields directly into your shopping cart page.

Implementing a Payment Gateway Integration in a Go App

In this section, you'll be taught how to write a Go web app with a checkout flow. Before you begin, you'll need to know how to write Go and use the Go toolchain. However, you don't need to have any familiarity with the Rapyd API.

You can follow along using the Rapyd-Samples/accept-payments repository. The go directory contains the complete demo app that you'll be building here.

Alternatively, you can implement all the steps directly by copying the code snippets from the article into a single file. Be sure to start this file with the package declaration and a list of required imports.

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)
Enter fullscreen mode Exit fullscreen mode

Note that if you decide to put the code snippets into multiple files, each file needs only a subset of imports. A Go-aware editor can add and remove imports automatically as needed.

For this example, you'll be building a small woolen-sock web shop that sells only one kind of socks. This means the home page will show the shopping basket right away. The user will be able to select the number of items they want to buy and then head to the checkout. This triggers the app to call the Create Checkout Page endpoint, which will create a new checkout page based on the total price of the items, the user's country, and the currency used.

Then, Rapyd takes over and handles the checkout until the user either completes or cancels the checkout flow. At that point, the flow is transferred back to the web shop.

You'll be building the app from the bottom up, so let's begin!

Set Up the Project

Create a new folder, cd into it, and run the following command:

go mod init rapyd-accept-payments
Enter fullscreen mode Exit fullscreen mode

If you plan to publish your project on GitHub, GitLab, or another repository host, use the full repository path instead of rapyd-accept-payments to enable the use of go install with the remote repository.

Get the Rapyd API Access Key and Secret Key

If you don't already have a Rapyd account, follow the prompts to sign up for one now.

Rapyd provides a sandbox mode for safe API development. After signing up or logging in, enable the sandbox mode by toggling the switch at the top of the page before you continue.

The sandbox switch

Then, navigate to Developers > Credential Details. Copy the two keys and set them as an environment variable in your shell.

export RAPYD_ACCESS_KEY=<your access key here>
export RAPYD_SECRET_KEY=your secret key here>
Enter fullscreen mode Exit fullscreen mode

This is for bash syntax. For other shells, the syntax may differ.

Your app will fetch these keys from the process environment when accessing the Rapyd API.

The API credentials page

Customize the Checkout Page

This step is optional because the hosted checkout page works right out of the box. However, while you're in the developer portal, you can check out the various options for customizing the checkout page to match your company branding.

To customize the checkout page, go to Settings > Branding. Here, you can adjust the look and feel for any hosted page, including the checkout page.

Checkout page branding

You could add your company logo, adjust the highlight color to match your company's color scheme, select the accepted payment methods, check the text on the Place Your Order button, or add customer support contact details. The changes are immediately visible in the preview area, which shows both desktop and mobile views.

Set Up the API Client

At this point in the tutorial, you're going to work with the API client, which contains the API authentication mechanism. Rapyd uses an advanced authentication scheme that generates a unique signature for every request that verifies the requester and also protects data from being tampered with in transit. This approach is far superior to using (semi-)static access tokens that can only verify the requester but not protect the data.

To authenticate, you need the two keys you acquired earlier, a timestamp, a salt, and a SHA-256 signature. While this sounds complicated, it's rather straightforward:

  • The salt is an 8- to 16-character random string. The demo app generates a 16-character hexadecimal string.
  • The timestamp is represented as Unix time, or "seconds since Jan 1, 1970, 00:00".
  • The signature is generated from a SHA-256 hash of these values:
    • Salt and timestamp
    • The HTTP method used (in lowercase)
    • The complete request body
  • The hash—or rather, the hex digest of it—is then Base64 encoded.

This is what the complete signature() function looks like:

func signature(httpMethod string, urlPath string, salt string, timestamp string, accessKey string, secretKey string, body string) string {

    // create a sha256 HMAC with the secret key
    hash := hmac.New(sha256.New, []byte(secretKey))

    // sign the request
    hash.Write([]byte(strings.ToLower(httpMethod) + urlPath + salt + timestamp + accessKey + secretKey + body))

    // get the hex digest of the hash and base64-encode it
    hexdigest := make([]byte, hex.EncodedLen(hash.Size()))
    hex.Encode(hexdigest, hash.Sum(nil))
    return base64.StdEncoding.EncodeToString(hexdigest)
}
Enter fullscreen mode Exit fullscreen mode

With this function, you can create a request() function that takes an HTTP method, a URL path representing the API endpoint, and a request body. It generates the signature, calls the API endpoint, and returns the response body or an error message.

The URL path must start with the API version number—for example, /v1/data/countries.

Please note: It's not recommended to use the default HTTP client from net/http if you want to set a request time-out. To avoid creating a new HTTP client every time request() is called, set up a RapydClient struct and make request() a method of that struct. Later, you will create a server struct that stores an instance of this client during its lifetime.

The following code is the request() function, along with the custom HTTP client:

type RapydClient struct {
    c *http.Client
}

func NewRapydClient() *RapydClient {
    return &RapydClient{
        c: &http.Client{
            Timeout: 10 * time.Second,
        },
    }
}

func (rc *RapydClient) request(method string, urlPath string, body []byte) ([]byte, error) {
    // turn the body into an io.Reader
    b := bytes.NewReader(body)

    // create a request with all headers required for authentication.
    req, err := http.NewRequest(method, "https://sandboxapi.rapyd.net"+urlPath, b)
    if err != nil {
        return nil, fmt.Errorf("Failed to create request: %v", err)
    }
    timestamp := fmt.Sprintf("%d", time.Now().Unix())
    salt := fmt.Sprintf("%016x", rand.Uint64())
    key := os.Getenv("RAPYD_ACCESS_KEY")
    secret := os.Getenv("RAPYD_SECRET_KEY")
    if key == "" || secret == "" {
        log.Fatalln("Please set the environment variables RAPYD_ACCESS_KEY and RAPYD_SECRET_KEY before starting the server.")
    }
    req.Header.Set("access_key", key)
    req.Header.Set("salt", salt)
    req.Header.Set("timestamp", timestamp)
    req.Header.Set("signature", signature(method, urlPath, salt, timestamp, key, secret, string(body)))

    // run the request and return the response body.
    resp, err := rc.c.Do(req)
    if err != nil {
        return nil, fmt.Errorf("Failed to send request: %v", err)
    }
    defer resp.Body.Close()
    respBody, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("Failed to read response body: %v", err)
    }
    return respBody, nil
}
Enter fullscreen mode Exit fullscreen mode

Create the "Create Checkout Page" API Call

With the HTTP client set up, it's time to build the specific Create Checkout Page call.

The minimum information that the client needs to pass over includes the following:

  • The amount of the payment
  • The currency
  • The user's country
  • A redirect URL that is called when the user finishes the checkout
  • Another redirect URL that is called when the user cancels the checkout

You can collect this data in a struct:

type CheckoutPage struct {
    Amount              float64 `json:"amount"`
    Country             string  `json:"country"`
    Currency            string  `json:"currency"`
    CompleteCheckoutURL string  `json:"complete_checkout_url"`
    CancelCheckoutURL   string  `json:"cancel_checkout_url"`
}
Enter fullscreen mode Exit fullscreen mode

Please note: The struct tags help translate the field names into JSON-compatible names.

As you can see, creating the checkout page is straightforward. You just fill the struct, marshal it into JSON format, call the Create Checkout Page API, and parse the response to get the redirect URL that starts the checkout flow at Rapyd's end. The Web server then redirects the browser to that URL.

The function that you'll create for these steps receives the selected number of items to set the total amount to pay. When running the app, try setting different item counts in the shopping cart and observe how the total amount on the checkout pages changes accordingly.

A note on the parsing of the API response: The Create Checkout Page call returns a large nested JSON structure with lots of useful info, but for this demo app, you need only the redirect URL (and the error information in case something goes wrong). This means you won't unmarshal the JSON into a Go struct. Instead, you'll unmarshal into maps and fetch the values from there.

func (s *Server) createCheckoutPage(host string, c int) (string, error) {

    // create the Rapyd checkout page with data from the basket.
    checkoutPage := CheckoutPage{
        Amount:              5.99 * float64(c),
        Country:             "US",
        Currency:            "USD",
        CompleteCheckoutURL: fmt.Sprintf("http://%s/complete", host),
        CancelCheckoutURL:   fmt.Sprintf("http://%s/cancel", host),
    }

    reqBody, err := json.Marshal(checkoutPage)
    if err != nil {
        return "", fmt.Errorf("error marshalling json: %w", err)
    }
    log.Println("createCheckoutPage payload:", string(reqBody))
    body, err := s.rapydClient.request("POST", "/v1/checkout", reqBody)
    if err != nil {
        return "", fmt.Errorf("error calling /v1/checkout: %w", err)
    }

    // parse the response dynamically and return the redirect URL
    var checkoutResponse map[string]any
    err = json.Unmarshal(body, &checkoutResponse)
    if err != nil {
        return "", fmt.Errorf("cannot unmarshal response from /v1/checkout: %w, body: %s", err, string(body))
    }
    status := checkoutResponse["status"].(map[string]any)
    if status["error_code"] != "" {
        return "", fmt.Errorf("error creating checkout page: %s: %s",
            status["status"],
            status["message"],
        )
    }

    data := checkoutResponse["data"].(map[string]any)
    return data["redirect_url"].(string), nil
}
Enter fullscreen mode Exit fullscreen mode

Add HTML Pages and HTTP Handlers

Now that the checkout page is in place, the next step is to set up the structure of the user-facing pages. This app will serve three pages and a form handler:

  1. The home page: This is also the simple shopping cart. It contains a form for selecting the number of pairs of socks to order.
  2. A form handler: The shopping cart form calls this handler to deliver the cart to the checkout process. The form handler takes the cart data and calls the Create Checkout Page API to initiate the checkout process.
  3. Two pages to which the checkout process returns: These are for when the user completes or cancels the checkout. These are the pages whose URLs you'll send to the Create Checkout Page API as redirect URLs.

To help keep the code short and simple, you don't need to use templates and can instead store the HTML for the pages in a map of plain static strings.

var (
    html map[string]string = map[string]string{
        "home": `<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>Rapyd Checkout Demo</title>
    </head>
    <body>
        <header>
            <h1>Rapyd Checkout Demo</h1>
        </header>
        <main>
            <form action='/checkout' method='POST'>
                <div>
                    <h2>Buy woolen socks</h2>
                </div>
                <div>
                    <label>Number of items:</label>
                    <input type='text' name='count' value='1'>
                </div> 
                <input type='submit' value='Continue to checkout'> </div>
            </form>
        </main>
    </body>
</html>`,
        "complete": `<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>Rapyd Checkout Demo</title>
    </head>
    <body>
        <header>
            <h1>Rapyd Checkout Demo</h1>
        </header>
        <nav>
            <a href='/'>Home</a>
        </nav>
        <main>
            <h2>Checkout complete</h2>
        </main>
    </body>
</html>`,
        "cancel": `<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>Rapyd Checkout Demo</title>
    </head>
    <body>
        <header>
            <h1>Rapyd Checkout Demo</h1>
        </header>
        <nav>
            <a href='/'>Home</a>
        </nav>
        <main>
            <h2>Checkout canceled</h2>
        </main>
    </body>
</html>`,
    }
)
Enter fullscreen mode Exit fullscreen mode

The home page handler displays the "home" element from the previous map.

Here, you use the standard router "ServeMux" from net/http. Due to the routing rules of ServeMux, the route "/" is a catch-all pattern if no other more specific routes match. The home page handler, therefore, needs to test for an exact match before serving the home page.

func (s Server) homeHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }

    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, html["home"])
}
Enter fullscreen mode Exit fullscreen mode

The checkout handler receives the form contents through a POST call and triggers the checkout flow by creating the checkout page and redirecting the browser to it.

If a GET request comes in, then most likely, the user has hit the browser back button. In this case, the checkout handler silently redirects to the home page.

func (s Server) checkoutHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Redirect(w, r, "/", http.StatusFound)
        return
    }

    err := r.ParseForm()
    if err != nil {
        log.Println("Failed to parse form:", err)
        http.Error(w, "Internal Error", http.StatusInternalServerError)
        return
    }
    itemCount, err := strconv.Atoi(r.Form.Get("count"))
    if err != nil {
        log.Println("Failed to parse itemCount:", err)
        http.Error(w, "Invalid item count entered", http.StatusInternalServerError)
        return
    }

    rapydCheckoutPage, err := s.createCheckoutPage(r.Host, itemCount)
    if err != nil {
        log.Println("Cannot create checkout page:", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, rapydCheckoutPage, http.StatusFound)
}
Enter fullscreen mode Exit fullscreen mode

The handlers for the /complete and /cancel pages are trivial. They display a message and provide a link to the home page.

func (s Server) completeHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, html["complete"])
}

func (s Server) cancelHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, html["cancel"])
}
Enter fullscreen mode Exit fullscreen mode

Wire the Routes to the Handlers and Start the Server

After setting up the handlers, you'll create a Server struct that holds a pointer to a RapydClient instance. Then you add a startServer() function that sets up a new router (ServeMux), wires the handlers to their routes, and finally starts the HTTP server on port 8080. (You'll need to update this if this specific port is already occupied on your machine.)

Now the web app is ready to run.

type Server struct {
    rapydClient *RapydClient
}

func startServer() {
    s := Server{
        rapydClient: NewRapydClient(),
    }
    // set up the router & wire the handlers to their routes
    mux := http.NewServeMux()
    mux.HandleFunc("/", s.homeHandler)
    mux.HandleFunc("/checkout", s.checkoutHandler)
    mux.HandleFunc("/complete", s.completeHandler)
    mux.HandleFunc("/cancel", s.cancelHandler)

    // create and run the server.
    server := &http.Server{Addr: ":8080", Handler: mux}
    log.Println("Server running on localhost:8080")

    err := server.ListenAndServe() // this call blocks until an error occurs.
    log.Println("Server error:", err)

}
Enter fullscreen mode Exit fullscreen mode

Complete the Code with a Main Function

With all of the above in place, the main function is a one-liner:

func main() {
    startServer()
}
Enter fullscreen mode Exit fullscreen mode

And with that, you're almost done!

If you happen to use a pre-1.20 Go toolchain, you need to seed the random number generator. Otherwise, the salt is always the same when you restart the app. Security-wise, this is not recommended, and the Rapyd API rightfully complains when it receives the same salt value twice. If you use Go 1.20 or later, you don't need this seed call.

func init() {
    rand.Seed(time.Now().UnixNano())
}
Enter fullscreen mode Exit fullscreen mode

Test the Final App

You're almost done. Now it's time to start the server and make it accessible to callbacks from Rapyd.

Run the Code

If the two environment variables are set, you can run the code right away or use the repo that contains the code in the go directory.

To run the server, cd into the source code directory and call the following:

go run .
Enter fullscreen mode Exit fullscreen mode

Make the Server Accessible for Callbacks

At the end of the checkout flow, the Rapyd API service redirects the browser back to your web-shop server, which means the URLs that you pass to the API for that purpose cannot be localhost URLs because the web-shop server needs to be reachable on the internet.

You can run the demo app on a (ephemeral) server, or you can use a proxy service like ngrok, which will generate a random web address for you and redirect all traffic to a specified port on your local machine.

In addition, remote development environments like Gitpod.io usually provide a similar service to expose local ports through a generated proxy URL. For more info, check out the documents for your remote dev environment provider.

Go through the Checkout Flow

To complete the checkout flow, open the public URL of the web shop in your browser. You should see a shopping cart page prefilled with your dream socks.

Shopping card

Enter the number of pairs of socks that you want to buy, and click Checkout. Then the hosted checkout page will open.

Payment page

Select a payment method, fill out the required fields, and click Place Your Order.

Place your order

At this point, a Thank You page should open and list out any steps required to complete the payment. These steps depend on the payment method chosen.

Once you've followed the prompts, click Finish.

Thank-you page

The flow redirects you back to the web shop, where the Checkout complete page opens.

Checkout complete

If you click the back arrow on the payment page instead of following through the checkout, you will see the Checkout canceled page instead.

Checkout canceled

Congrats! You completed your first Rapyd checkout flow.

Conclusion

In this tutorial, you created a basic checkout flow where most of the code is standard Go web app code—and this is only the start! Rapyd is so much more than a checkout solution. Rapyd provides a suite of payment and financial services for businesses through a fast and easy-to-use platform, including cross-border transactions, digital wallets, and payment processing.

Check out the Rapyd API now and bring your online business to the next level.

Have any thoughts about this tutorial? We'd love to hear them! Post any thoughts or related questions in the Rapyd Developer Community.

Top comments (0)