Separate your product catalog from commerce.

Tutorials

Building an external payment gateway with the Twitter API.

August 17, 2022 Bolaji Ayodeji

In the previous article, we explored payment gateways in detail and how businesses can increase sales by improving their checkout process and providing more creative and local payment methods that customers love and trust. Remember that 18% of people from the Baymard summary said they don’t trust websites with their credit card data. This is why we made integrating with any local payment gateway possible so you can explore any option that your customers demand. In this article, we will take a step further and show you how to get started with Commerce Layer payment gateways, external payment gateways, and payment methods. Ultimately, we will create a demo “Pay With Tweet” external payment gateway that will enable customers to pay for their order by just making a tweet without any cash transaction. Sounds weirdly cool, right? Well, let’s get into it, and you’ll learn more about it.


Prerequisites

  • A general understanding of how APIs and the command-line work.
  • Some prior knowledge of the JavaScript programming language.
  • You have a Commerce Layer account already.
  • You have set up your organization and created the required commerce data resources for your market. You can follow the onboarding tutorial or manual configuration guide to achieve this.

The Pay with Tweet gateway

External payment gateways enable you to connect any payment gateway with Commerce Layer beyond those integrated natively. To make it clear that the payment gateway can be “any,” we decided to build a creative one: the “Pay with a Tweet” gateway. The idea is that when a customer purchases from your store, they’re prompted to make a tweet with a predefined text that includes their order number. The external payment gateway then reads the Twitter API for the tweet and authorizes the order if the tweet is found. All this happens with the customer not having to make a monetary payment. Ideas like this are useful when you’re building a store for, let’s say, free swag distribution at developer events or free items for customers that qualify for a certain promotion. Instead of just giving out the items “directly free,” asking the customer to make a tweet to publicize your business to their Twitter network seems like a fair deal 😎. This becomes an exchange of an item for peer-to-peer publicity/marketing rather than the traditional item for money (imagine using this for sales in a conference or group event; cool right?!).

We will work with the Commerce Layer CLI, Twitter API, Lambda functions, and Commerce Layer external gateways. At the end of this article, you’d have learned how to use all the tools and services mentioned above.

The fundamentals of Commerce Layer payment gateways

As discussed in the previous article, Commerce Layer supports the ability to integrate with different available and popular payment options. By the time you are ready to add an external payment gateway, we assume you have created an organization, created a merchant, configured your markets, have some items ready for sale, and you are ready to start taking orders. The diagram below illustrates the relationships between all the resources associated with payments.

Now, let’s talk about the payment gateway options Commerce Layer supports.

PSD2-ready payment gateways

Commerce Layer provides out-of-the-box integrations with the most popular payment gateways (Stripe, Klarna, PayPal, Adyen, Braintree, etc.). All these integrations comply with the PSD2 European regulation so that you can implement a payment flow that supports SCA and 3DS2. You can read our payment gateways documentation to learn how to create and use any of these gateways. Generally, you can achieve this using our dashboard, API, or CLI by creating the type of gateway you want and providing the required credentials.

Here’s an example API request to create a Stripe payment gateway:

curl -g -X POST \
  'https://yourdomain.commercelayer.io/api/stripe_gateways' \
  -H 'Accept: application/vnd.api+json' \
  -H 'Authorization: Bearer your-access-token' \
  -H 'Content-Type: application/vnd.api+json' \
  -d '{
  "data": {
    "type": "stripe_gateways",
    "attributes": {
      "name": "EUR payment gateway",
      "login": "sk_live_xxxx-yyyy-zzzz"
    }
  }
}' 

Here’s an example CLI command to create an Adyen payment gateway:

cl create adyen_gateways -a \
	name="US payment gateway" \
	merchant_account="<your merchant account>" \
	api_key="<your api key>" \
	live_url_prefix="1797a841fbb37ca7-AdyenDemo"

The processes above are similar when creating other gateways. Don’t forget to explore our payment gateway documentation to get started.

Manual payment gateways

With Commerce Layer, you can process payments through wire transfer, cash, and other manual payment options. The process to achieve this is similar to the credit-card gateways. You will need to create the manual payment gateway, and when a customer creates an order, you will associate a manual payment source with the order. Kindly check out our documentation or how-to guide below to get started with manual payment gateways.

External payment gateways

For an external payment gateway to work, the payment source associated with the order must be an external payment. When you create an external gateway, you will pass a set of resource arguments in the request body, including custom endpoint(s) responding to the following transaction types:

  1. Authorization: this will authorize a payment source (verify that the required funds are available in the customer's payment source).
  2. Capture: this will capture an authorization (transfer the money from the customer payment source to the merchant).
  3. Void: this will void an authorization (cancel the authorization on the customer's order).
  4. Refund: this will refund a capture, either totally or partially.
  5. Customer token: this will create a customer payment token.

The associated external endpoint(s) will be responsible for integrating with the payment gateway, including authorizing, capturing, voiding, and refunding an order. For example, let’s say you want to capture the authorization for an order. Assuming you have tokenized a credit card on the client side, you create an external payment with the token, you associate it to the order as the payment source, and when you place the order, Commerce Layer then triggers a POST request to the endpoint specified in the authorize_url field with some payload including the orders, markets, line items, skus, customers, shipments, etc. data. If the transaction is successful, a JSON response is sent back to Commerce Layer, returning a transaction token (provided by the payment gateway), the total amount that has been captured, and some metadata as seen below:

{
  "success": true,
  "data": {
    "transaction_token": "your-external-transaction-token",
    "amount_cents": 20950,
    "metadata": {
       "foo": "bar"
        }
     }
}

If the transaction is unsuccessful, a similar response is sent back to Commerce Layer with an error message as seen below:

{
  "success": false,
  "data": {
    "transaction_token": "your-external-transaction-token",
    "amount_cents": 20950,
    "error": {
      "code": "YOUR-ERROR-CODE",
      "message": "Your error message"
    }
  }
}

The same responses are expected when you capture, void, or refund an order.

Building the Pay With Tweet gateway

Now the sweet part of this tutorial 😋. Here’s a breakdown of the entire flow of the system:

Customer purchase

  • Visit the store (a market associated with an organization).
  • Create a customer (associated with an email address).
  • Create an order associated with the customer.
  • Add some SKUs to the order.
  • Add other required resources to the order.
  • Make a tweet with predefined text (some text, hashtags, and a tag to a Twitter account representing the organization:market the customer is purchasing from).
  • Place the order.

Payment gateway

  • Receive some payload from Commerce Layer when the external payment gateway is triggered.
  • Fetch tweets from the defined Twitter account (that represents the Commerce Layer organisation:market) between when the order was last updated and 30 minutes after.
  • Sort, filter, and read the tweets for one with the order number associated with the payload received.
  • If found, authorize the transaction and place the order.
  • If not found, return an error and fail the transaction.

Extras

  • Send an email to the customer with proof of purchased product.
  • Send the customer information to fulfill the shipment.
  • Et cetera.

The next sections will show you how to set up the Commerce Layer CLI, create an external payment gateway, build the actual integration, and place an order using the CLI. Ideally, this flow will work on your sales channel, where a customer will select some items from your product listing, add them to cart, and checkout the order. When they select a payment method, they will have the option to select the “Pay With Tweet” option, which will show them the predefined tweet (with a link to make the tweet automatically), and once the customer clicks something like “share tweet now,” your sales channel will trigger the external payment gateway and checkout the order if the transaction is successful. By the time you understand how it works with the CLI, you will be able to easily make the right API calls to add this integration into your already existing store running on your choice of technology and tools.

Getting started with Commerce Layer CLI

The Commerce Layer CLI helps you to manage your Commerce Layer applications and resources right from the terminal. Kindly install the CLI using your favorite package manager:

//npm
npm install -g @commercelayer/cli

//yarn
yarn global add @commercelayer/cli

Installing the CLI provides access to the commercelayer command that can also be accessed with the cl alias. Follow this guide in the documentation to create an integration application and save the provided client ID, client secret, and base endpoint credentials.

Setting up the external payment gateway

Kindly follow the steps below to create an external payment gateway using the CLI:

1. Create a new payment gateway associated with a name and optional URLs that point to the endpoint that will process the transaction types.
cl create external_gateways -a \
	name="Pay With Tweet" \
	authorize_url="" \
	capture_url="" \
	void_url="" \
	refund_url="" \
	token_url=""

Since we haven’t created our custom endpoint yet, you can leave the URLs empty or remove them entirely (you can always update the gateway after it’s created). When you have successfully created the external payment gateway, a resource object is returned:

{
  id: 'PkXRMsBBGv',
  type: 'external_gateways',
  name: 'Pay With Tweet',
  created_at: '2022-07-09T06:26:38.122Z',
  updated_at: '2022-07-09T06:26:38.122Z',
  reference: null,
  reference_origin: null,
  metadata: {},
  shared_secret: 'a51019c761a032ee50b10aa59802ded1',
  authorize_url: 'null',
  capture_url: null,
  void_url: null,
  refund_url: null,
  token_url: null
}
2. Create a new payment method associated with a market and payment gateway like so:
cl create payment_methods -a \
	payment_source_type="ExternalPayment" \
	currency_code="USD" \
	price_amount_cents=0 -r \
	market="YlqxGhKrQg" \
	payment_gateway="PkXRMsBBGv"

You can get your market ID via the dashboard or by running the command cl list markets. When you have successfully created the payment method, a resource object is returned:

{
  id: 'wmBvQsAzBm',
  type: 'payment_methods',
  payment_source_type: 'external_payments',
  name: 'External Payment',
  currency_code: 'USD',
  moto: false,
  disabled_at: null,
  price_amount_cents: 0,
  price_amount_float: 0,
  formatted_price_amount: '$0.00',
  created_at: '2022-07-09T06:47:05.841Z',
  updated_at: '2022-07-09T06:47:05.841Z',
  reference: null,
  reference_origin: null,
  metadata: {},
  market: {
    id: 'YlqxGhKrQg',
    type: 'markets',
    number: 9480,
    name: 'USA',
    facebook_pixel_id: null,
    checkout_url: '',
    external_prices_url: '',
    private: false,
    created_at: '2022-02-22T04:01:34.336Z',
    updated_at: '2022-05-31T08:57:24.446Z',
    reference: 'market_2',
    reference_origin: 'CLI',
    metadata: {}
  },
  payment_gateway: {
    id: 'PkXRMsBBGv',
    type: 'payment_gateways',
    name: 'Pay With Tweet',
    created_at: '2022-07-09T06:44:34.692Z',
    updated_at: '2022-07-09T06:44:34.692Z',
    reference: '',
    reference_origin: '',
    metadata: {}
  }
}

The steps above comprise the essentials you will need to work with an external payment gateway. Your external endpoint will be responsible for the actual integration with the payment gateway. Now, let’s proceed to build that!

Getting Started with Twitter API

Twitter’s API allows you to interact with data on the Twitter platform. Working with the Twitter API is quite exciting; you can read their documentation or follow the quick steps below to get started:

  1. Sign up for a Twitter developer account.
  2. Create a new project:
    • Enter project name (e.g., CL Twitter Payment Gateway)
    • Select a use case (e.g., Building tools for businesses)
    • Enter a project description
  3. Create a new application:
    • Select “development”
    • Enter the development app name (e.g., Pay With Tweet)
  4. Copy your keys and access tokens:
    • API Key and Secret
    • Bearer Token
    • Access Token and Secret
  5. You can use the app settings to update any detail you wish.

Building the gateway with Twitter SDK, Nodejs, Express, and Ngrok

We will build a custom /authorize endpoint responsible for receiving the payload from Commerce Layer once an order is placed, processing the order, and authorizing the transaction. We’ll use TypeScript, Nodejs, and Express to build the server and then utilize Ngrok to tunnel our local server on the internet. Let’s get right into it!

1. Install the Twitter API Typescript SDK with the command below:
npm install twitter-api-sdk
2. Install tsc and tsc-watch for compiling Typescript to JavaScript if you haven’t yet with the command below:
npm install tsc

npm install tsc-watch
3. Install Node.js if you don’t have it yet.
4. Install Express if you don’t have it yet with the command below:
//npm
npm install express --save

//yarn
yarn add express
5. Install ngrok using the commands below or download it for any operating system:
//mac
brew install ngrok

//linux
snap install ngrok

//windows
choco install ngrok
6. Create a new ngrok account if you don’t have one yet.
7. Create a new API key and copy the generated token.
8. Configure ngrok with the auth token using the command below:
ngrok config add-authtoken 12wLGWJHEFWLTGUKPsYphK3g2FGKk4s_3gyhbDFJHs5JzJ
9. Install CryptoJS (we will use this to verify the callback authenticity from Commerce Layer):
npm install crypto-js
10. Create a tsconfig.json file and add the following code:
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "commonjs",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "public",
    "typeRoots" : ["typings", "./node_modules/@types"]
  },
  "include": [
    "api/*",
    "typings/*"
  ]
}
11. Create a typings/express/index.d.ts file and add the following code:
declare module "http" {
  interface IncomingMessage {
      rawBody: any;
  }
}
12. Create a .env file and add the following credentials:
  • The shared secret generated when you created the external payment gateway earlier (you can find this on the Commerce Layer dashboard).
  • The ID of the Twitter account you want to fetch tweets from (you can find the ID of your Twitter account using this platform).
  • The bearer token associated with the Twitter app you created earlier.
CL_SHARED_SECRET=""
CL_TWITTER_ID=""
TW_BEARER_TOKEN=""
13. Create an /api directory, create a getTweets.ts file inside the directory, and add the following code to fetch 5 tweets (including the user’s data) that mentions the specified Twitter account within a defined time range:
require("dotenv").config();
import { Client } from "twitter-api-sdk";

const client = new Client(process.env.TW_BEARER_TOKEN as string);
const clTwitterId = process.env.CL_TWITTER_ID as string;

// Get tweets and users that mentions the clTwitterId
async function getTweets(startTime: string, endTime: string) {
  const mentions = await client.tweets.usersIdMentions(clTwitterId, {
    start_time: startTime,
    end_time: endTime,
    max_results: 5,
    "tweet.fields": ["author_id", "id", "created_at", "text"],
    expansions: ["author_id"],
    "user.fields": ["id", "username", "name"]
  });

  const tweets = mentions.data || [];
  const users = mentions.includes?.users || [];
  return {tweets, users};
}

export default getTweets;
14. Create an app.ts file in the /api directory, and add the following code to create an /authorize endpoint that will process the transaction:
require("dotenv").config();
import express, { Request, Response } from "express";
import getTweets from "./getTweets";

const CryptoJS = require("crypto-js");
const hmacSHA256 = require("crypto-js/hmac-sha256");
const app = require("express")();
const port = process.env.PORT || 9000;

interface tweetUsers {
  id: string | undefined;
  username: string | undefined;
  name: string | undefined;
  email: string;
  tweet: string;
  created_at: string | undefined;
}

// Use the JSON middleware and verify handler to attach a rawBody attribute to the req object.
app.use(
  express.json({
    limit: "5mb",
    verify: (req, res, buf) => {
      req.rawBody = buf.toString();
    }
  })
);

app.post("/authorize", (req: Request, res: Response) => {
  // Verify the payload coming from Commerce Layer.
  const signature = req.headers["x-commercelayer-signature"];
  const hash = hmacSHA256(req.rawBody, process.env.CL_SHARED_SECRET);
  const encode = hash.toString(CryptoJS.enc.Base64);

  if (req.method === "POST" && signature === encode) {
    const payload = JSON.parse(req.rawBody);

    // Get the included orders data from the payload.
    payload.included.slice(0, 1).map((order: any) => {
      const startTime = order.attributes.updated_at;
      const endTime = new Date(
        new Date(startTime).getTime() + 1800000
      ).toISOString();
      const orderNumber = order.attributes.number;
      const customerEmail = order.attributes.customer_email;
      const amountCents = order.attributes.subtotal_taxable_amount_cents;
      const transactionToken = order.attributes.token;

      // Fetch tweets created between startTime and endTime.
      getTweets(startTime, endTime).then((data) => {
        // If tweet(s) found, process the transaction.
        if (data.tweets.length !== 0) {
          const tweets = data.tweets;
          const users = data.users;
          let customerData: tweetUsers;

          tweets.map((tweet) => {
            // Find the tweet created by the customer.
            if (tweet.text.includes(orderNumber)) {
              customerData = {
                id: tweet.author_id,
                username: users.find((u) => u.id === tweet.author_id)?.username,
                name: users.find((u) => u.id === tweet.author_id)?.name,
                email: customerEmail,
                tweet: tweet.text,
                created_at: tweet.created_at
              };
              // If successful, authorize the transaction.
              res.status(200).json({
                success: true,
                data: {
                  transaction_token: transactionToken,
                  amount_cents: amountCents,
                  metadata: {
                    ...customerData
                  }
                }
              });
            } else {
              // If unsuccessful, return an error, and fail the transaction.
              res.status(422).json({
                success: false,
                data: {
                  transaction_token: transactionToken,
                  amount_cents: amountCents,
                  error: {
                    code: "404",
                    message: `Order number: ${orderNumber} not found in tweets`
                  }
                }
              });
            }
          });
        } else {
          // If tweet(s) not found, return an error, and fail the transaction.
          res.status(422).json({
            success: false,
            data: {
              transaction_token: transactionToken,
              amount_cents: amountCents,
              error: {
                code: "404",
                message: `Tweet with order number: ${orderNumber} not found.`
              }
            }
          });
        }
      });
    });
  } else {
    // If signature does not match the shared secret, return an error.
    res.status(401).json({
      error: "Unauthorized: Invalid signature"
    });
  }
});

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
});
15. Build and start the local Node.js server to begin listening on port 9000 like so:
npx tsc && node public/app.js
16. Start an HTTP tunnel listening for HTTP/HTTPS traffic on port 9000:
ngrok http 9000

Now we can use https://c54e-197-210-70-223.eu.ngrok.io/authorize as our authorize_url to receive the payload from Commerce Layer and process the transaction (PS: that the URL will be different when you do this yourself, and it will change every time you restart the ngrok tunnel). You can consider deploying the Nodejs application online for a permanent URL. You should update the previously created external payment gateway with the authorize_url.

cl update external_gateways/PkXRMsBBGv -a authorize_url="https://c54e-197-210-70-223.eu.ngrok.io/authorize"


Testing the Pay With Tweet external payment gateway

Now let’s test the custom payment gateway by placing an order. This section will highlight the core fundamentals and lifecycle of orders in the Commerce Layer. Ideally, this flow will be used by your customer on a sales channel or any other choice of customer experience. For example, you might build your ecommerce web store and consider using our hosted checkout as a boilerplate to build a checkout experience for your external payment gateway. With Commerce Layer, you can do pretty much anything you want by composing multiple tools together. But for the sake of this tutorial, we will use the CLI to complete an order purchase. By doing this, you will learn all the requirements for placing orders and can use the same flow when working with our API directly.

1. Log into your application via the CLI using the previously created integration application credentials like so:
cl applications:login -o <organizationSlug> -i <clientId> -s <clientSecret> -a <applicationAlias>

The application alias can be named anything. It will be associated with the application you want to log into on your terminal (this is useful when switching between multiple applications).

2. List all SKUS and copy the ID of any one using the command below:
cl get skus
3. List all markets and copy the ID of any one using the command below:
cl get markets
4. Create an order associated with a market ID and save the returned order ID/number using the command below:
cl create orders -r market=markets/YlqxGhKrQg
5. Create a line item associated with an SKU ID and order ID using the command below:
cl create line_items -a \
	quantity=1 -r \
	order=orders/NZrQhxDVXX -r \
	item=skus/ZwpOSrMAzP
6. Update the order with a customer using the command below:
cl update orders/NZrQhxDVXX -a customer_email=bolaji@commercelayer.io
7. Create an address using the command below:
cl create addresses -a \
	first_name="Bolaji" \
	last_name="Ayodeji" \
	line_1="No 3, House B, Sample test street" \
	city="San Francisco" \
	zip_code=94105 \
	state_code="CA" \
	country_code="US" \
	phone=1234567890
8. Update the order’s shipping and billing address with an address ID using the command below:
cl update orders/NZrQhxDVXX -r \
	billing_address=addresses/BnNguAkzyX \
	shipping_address=addresses/BnNguAkzyX
9. Get the shipments (this is automatically created once an order is created) and available shipping methods (you should have created some for your market already) for the order using the command below:
cl rget orders NZrQhxDVXX -i \
	shipments,shipments.available_shipping_methods
10. Update the shipment with any shipping method of your choice using the command below:
cl update shipments/PVNXCkGlLx -r \
	shipping_method=shipping_methods/qVWbjFlAWN
11. Get available payment methods using the command below:
cl get orders NZrQhxDVXX -i available_payment_methods
12. Update order with payment method using the command below:
cl update orders/NZrQhxDVXX -r \
	payment_method=payment_methods/wmBvQsAzBm
13. Create an external payment source and add it to the order using the command below. For an external payment gateway to work, the payment source associated with the order must be an external payment.
cl create external_payment \
	-a payment_source_token="testToken12345" \
	-r order=orders/NZrQhxDVXX
14. Make a tweet mentioning @commercelayer and include the order number like so:
Thanks @commercelayer for the free BACKPACKFFFFFF000000XXXX swag (order number: 25114555).

#commercelayer #composablecommerce
15. Place the order using the command below:
cl update orders/NZrQhxDVXX -a _place=true

If no tweet is found between the timestamp the order was last updated and 30 minutes or no tweet found contains the order number, the CLI will return this error:

 ›   Error: [
 ›     {
 ›       title: 'Unprocessable Entity',
 ›       detail: 'Unprocessable Entity',
 ›       code: 'VALIDATION_ERROR',
 ›       source: { pointer: '/data' },
 ›       status: '422',
 ›       meta: { error: 'Unprocessable Entity' }
 ›     },
 ›     {
 ›       title: 'authorization failed',
 ›       detail: 'authorization failed',
 ›       code: 'VALIDATION_ERROR',
 ›       source: { pointer: '/data' },
 ›       status: '422',
 ›       meta: { error: 'authorization_failed' }
 ›     },
 ›     {
 ›       title: 'transition is not permitted',
 ›       detail: 'status - transition is not permitted',
 ›       code: 'VALIDATION_ERROR',
 ›       source: { pointer: '/data/attributes/status' },
 ›       status: '422',
 ›       meta: { error: 'not_permitted_transition' }
 ›     }
 ›   ]

And the endpoint will return these errors to Commerce Layer with the associated data.

If the tweet is found that includes the order number, the CLI will update the order’s status to placed and payment_status to authorized.

{
  id: 'NZrQhxDVXX',
  type: 'orders',
  number: 25114555,
  autorefresh: true,
  status: 'placed',
  payment_status: 'authorized',
  fulfillment_status: 'unfulfilled',
  guest: true,
  editable: false,
  customer_email: 'bolaji@commercelayer.io',
  language_code: 'en',
  currency_code: 'USD',
  token: '2bb69b735f125eeab22fd34900c75689',
	...
}

And the endpoint will return a success=”true” response to Commerce Layer with the associated metadata.

You can confirm the order’s status via the dashboard or with the CLI using the command below and proceed to approve and fulfill the order. You might also want to use our webhooks to send an email to the customer further once the order is placed or send the customer’s details to your external fulfillment service. Remember, with Commerce Layer; your options are limitless :).

cl get orders/NZrQhxDVXX

Conclusion

We believe you now understand how to add any payment gateway anywhere with Commerce Layer. Once an order is placed successfully, the following actions are performed asynchronously. In summary, this is how the payment process of an order works in Commerce Layer for both credit card and external payment gateways when a customer makes a purchase of some SKUs in a market:

  • Create an order
  • Assign the order to the customer
  • Assign the order to a shipping address
  • Assign the order to a billing address
  • Assign the order’s shipment with a shipping method
  • Assign the order with a payment method
  • Assign the order with a payment source
  • Add a coupon if required
  • Place the order
  • Authorize the transaction
  • Void the transaction if required (the end)
  • Capture the transaction
  • The end

Now feel free to bring all the ideas and customer demands you have to life. Your goal is to satisfy your customers, retain them, and increase your revenue. The good news is that Commerce Layer is here to help you do just this and more; cheers!