Zero
Zero
Back

Creating a Payment Categorization API with OpenAI's GPT

The world's most powerful AI models are built using extremely advanced machine learning and run on expensive specialized hardware. But it's actually incredibly easy to integrate these models' functionality into your application, as I'll show in this article.

Sam Magura

Sam Magura

An unwound fiber optic cable

GPT-4  by OpenAI is arguably the most advanced AI model that is publicly available right now (albeit in a limited beta). When provided with a well-crafted prompt, GPT can provide insightful responses to a wide variety of questions and requests. It can answer fact-based questions about the world, find bugs in snippets of code, and write advertisements for products, just to name a few things.

While the technology used to train and operate GPT is extremely advanced, it's actually incredibly easy to integrate its AI smarts into your applications via the OpenAI API . In this article, we'll use GPT and the OpenAI API to create a payment categorization service. Our service will expose a REST API that accepts a description of a service from a billing invoice, like "Tire rotation and oil change". Then, the API will respond with the category into which that services falls. In this case, we would expect a response of "Automotive service" or similar.

Creating an API like this would be a nearly impossible task without the help of an AI model — it's simply not feasible to write a traditional algorithm that can accurately categorize arbitrary payments. But with the OpenAI API doing all of the heavy lifting, it's actually quite simple — and this article will show you how, in a step-by-step guide.

Secure your secrets conveniently

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.

Zero dashboard

Project Overview

Our payment categorization REST API will be implemented in TypeScript using Express , the most popular Node.js framework for HTTP APIs. Our API will have a single endpoint which accepts a service description and then formulates a prompt that instructs the AI model to give us the category of the service. Our code will pass the prompt to the OpenAI API via the openai npm package . We'll use the Zero secrets manager to store our OpenAI API key and then fetch it at runtime using the Zero TypeScript SDK . Finally, our code will process the response from OpenAI and return the category to the consumer of our API.

🔗 The full code for this example is available in the zerosecrets/examples  GitHub repository.

Signing Up for the OpenAI API

Getting access to the OpenAI API is free and easy. Simply click here  and click the "Sign up" button.

Once you're logged in, click your profile picture in the upper right of the screen and click "View API keys". From this screen, click "Create new secret key", and copy the API key to a safe location on your local computer.

The API keys screen in the OpenAI API dashboard

Now, let's put the OpenAI API key in Zero for safekeeping. Log into your Zero account, create a new project, and copy the project's Zero token to that same safe location on your local computer. Now click the "New secret" button and fill in the fields as shown here:

Creating the OpenAI secret in Zero

Click the "Create" button to finish. Congratulations, you've stored your API key securely in the Zero secrets manager and can now retrieve it from your application.

Creating the Express REST API

Let's walk through setting up a new Node.js application using the Express web framework. I'm not aware of any CLI tools for instantly bootstrapping a minimal Express project, so we need to do some manual setup.

Here are the steps to follow:

  1. Create a new directory called payment-categorization-api for your project.

  2. Enter the directory with cd payment-categorization-api.

  3. Install Express and TypeScript:

    1
    shell
    npm install express @types/express typescript
    

    This will automatically create a package.json for you.

  4. Create a tsconfig.json with the following settings.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    json
    {
      "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022"],
        "module": "ESNext",
        "moduleResolution": "node16",
        "esModuleInterop": true,
        "isolatedModules": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "outDir": "dist"
      },
      "include": ["src"]
    }
    
  5. Create a src directory and add an empty main.ts file to it. This is the entrypoint to our application.

  6. Add "type": "module" to the package.json to tell Node that our JavaScript files are ES Modules, not CommonJS modules.

  7. Add a start script to package.json that compiles our code using the TypeScript compiler and then runs it with Node:

    1
    2
    3
    json
      "scripts": {
        "start": "tsc && node dist/main.js"
      },
    

Feel free to run npm start to verify that everything is set up correctly. The program won't output anything, since we haven't added any code yet, but you should get an exit code of 0, which indicates that the program completed successfully. You can check the exit code like this:

1
2
shell
npm start
echo $?

Adding an API Endpoint

If you completed the last section successfully, great job — that was the hard part. Adding a REST API is straightforward now that the project has been bootstrapped.

Place the following code in main.ts to add an Express web server to the app:

1
2
3
4
5
6
7
8
9
10
11
typescript
import express from 'express'

const PORT = 3000

const app = express()

app.use(express.json())

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}...`)
})

The code is pretty self-explanatory, except for the line app.use(express.json());, which tells Express to parse the incoming request body as JSON before calling our handler function.

Now, let's add a POST /api/payment/category API method which returns the category for the payment based on a description of the service that was performed.

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript
app.post('/api/payment/category', async (req, res) => {
  const serviceDescription = req.body?.serviceDescription

  if (typeof serviceDescription !== 'string') {
    res.status(400).send()
    return
  }

  // TODO Get the category from the OpenAI API
  const category = 'test'

  res.send(JSON.stringify(category))
})

💡 I chose to use POST instead of GET for my API method so that the client can send the service description in the request body. GET might be more technically correct since our API method is a query, not a mutation, but POST provides a better developer experience to the consumers of our API.

With that in place, start up the app with npm start and let's test out the API method. If you're building an API with more than a couple of endpoints, I recommend using Postman  to test your API. That said, the curl command line tool can be more convenient for testing a super simple API like the one we're creating. Here's the curl syntax for calling the API method:

1
2
3
4
5
shell
curl --location 'http://localhost:3000/api/payment/category' \
--header 'Content-Type: application/json' \
--data '{
    "serviceDescription": "Tire rotation"
}'

If everything worked properly, you should get back the JSON string "test".

API complete! Now it's time for the fun part — integrating with OpenAI and GPT.

Creating an OpenAI API Client

OpenAI provides an easy-to-use client for their API which is available on npm . In this section, I'll show you how to initialize the OpenAI API client, using the Zero TypeScript SDK  to securely retrieve the API key from Zero.

First, install the npm packages for OpenAI and Zero:

1
shell
npm install openai @zerosecrets/zero

Then, we'll write an asynchronous function that calls Zero and uses the API key to instantiate the OpenAI API client. This is the exact same pattern we have followed in many other posts, like The Quickest Way to Set Up Stripe in a Web App.

Create the file src/getOpenAIClient.ts and paste in the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typescript
import {zero} from '@zerosecrets/zero'
import {OpenAIApi, Configuration} from 'openai'

let openai: OpenAIApi | undefined

export async function getOpenAIClient(): Promise<OpenAIApi> {
  // Reuse the same OpenAI client if one has already been created, so that we
  // don't call Zero on every request
  if (openai) {
    return openai
  }

  if (!process.env.ZERO_TOKEN) {
    throw new Error('Did you forget to set the ZERO_TOKEN environment variable?')
  }

  const secrets = await zero({
    token: process.env.ZERO_TOKEN,
    pick: ['openai'],
  }).fetch()

  if (!secrets.openai) {
    throw new Error('Did not receive an API key for OpenAI.')
  }

  const configuration = new Configuration({
    apiKey: secrets.openai.api_key,
  })
  return new OpenAIApi(configuration)
}

Let's break down what this code is doing:

  1. Check if we've previously instantiated an OpenAI API client. If we have, return the existing client so that we don't make an unnecessary API call to the Zero API.
  2. Use zero({ ... }).fetch() to exchange our Zero token for the OpenAI secret. This expects the Zero token to passed in via an environment variable named ZERO_TOKEN. This line of code will cause the Zero SDK to make a request to the Zero GraphQL API .
  3. Create a Configuration object using the OpenAI API key we got from Zero.
  4. Create an OpenAIApi API client using the Configuration object.

Connecting the Pieces

Next, we will create a getPaymentCategory function to bridge the gap between the Express REST API and the OpenAI API. The Express API handler will call getPaymentCategory like this:

1
typescript
const category = await getPaymentCategory(serviceDescription)

getPaymentCategory will use the getOpenAIClient function we created in the previous section, and then execute the openai.createCompletion function to leverage GPT. We'll be using the text-davinci-003 model , which is part of GPT 3.5. (GPT 4 is currently only available in a limited beta as of this writing.)

For now, we'll just pass in a generic test prompt — we'll design the real prompt in the next section.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typescript
import {getOpenAIClient} from './getOpenAIClient.js'

const UNKNOWN_CATEGORY = 'Unknown'

export async function getPaymentCategory(serviceDescription: string): Promise<string> {
  const prompt = 'What is your name?'

  const openai = await getOpenAIClient()

  // These are the default options for `temperature`, `top_p`, .etc, as shown
  // in the examples from the OpenAI API dashboard
  const response = await openai.createCompletion({
    model: 'text-davinci-003',
    prompt,
    temperature: 0.5,
    max_tokens: 100,
    top_p: 1.0,
    frequency_penalty: 0.0,
    presence_penalty: 0.0,
  })

  // TODO Process the response

  return UNKNOWN_CATEGORY
}

Let's add a console.log(response.data) to the above code and run the project to see what OpenAI returns. Now that we've integrated with Zero, you'll need to pass your Zero token in as an environment variable when running the application:

1
shell
ZERO_TOKEN='YOUR_ZERO_TOKEN' npm start

Then, use curl or Postman to trigger the payment categorization API method like we did before. In the terminal where the server is running, you'll see the response from OpenAI which will be similar to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript
{
  id: 'cmpl-7LWpU7gIEwvVReLrK9Rk8jOId2p9D',
  object: 'text_completion',
  created: 1685365440,
  model: 'text-davinci-003',
  choices: [
    {
      text: '\n\nMy name is John.',
      index: 0,
      logprobs: null,
      finish_reason: 'stop'
    }
  ],
  usage: { prompt_tokens: 5, completion_tokens: 7, total_tokens: 12 }
}

As you can see, the response contains various metadata about our request, like the number of tokens in the prompt. The part we care about is the text completion itself, which is in the choices array under the text property. (Aside: It's quite interesting that GPT thinks it's name is John.)

Our REST API should return only the text completion, so let's add some code to getPaymentCategory that extracts the completion from response.data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript
if (response.status >= 400) {
  throw new Error(`OpenAI returned an error status code: ${response.status}.`)
}

if (response.data.choices.length === 0) {
  return UNKNOWN_CATEGORY
}

const choice = response.data.choices[0]
const text = choice.text?.trim()

if (!text) {
  return UNKNOWN_CATEGORY
}

return text

As always, it's best to practice "defensive coding" by not assuming that the OpenAI API call will be successful, or that the choices array will be non-empty.

Crafting the Prompt

The usefulness of GPT varies widely based on the quality of prompt you provide it. Fortunately, our use case is pretty simple and therefore does not require a complex prompt. There's just one gotcha to be aware of. If we give GPT the prompt

An invoice was received for the following service. Tell me the category of the service.

Service: Tire rotation

It will return a completion like

The category of the service is automotive maintenance.

While this is a correct and useful answer to our question, it's not a suitable response for our API method, which should return just the category without "The category of the service is".

To fix this, all we have to do is tell GPT that we want only the category. Here's the final prompt, which you can paste into the beginning of the getPaymentCategory function:

1
2
3
4
5
typescript
const prompt =
  `An invoice was received for the following service. ` +
  `Tell me the category of the service.` +
  `Give me only the category so I can copy paste.\n\n` +
  `Service: ${serviceDescription}`

If you test the payment categorization API method again, you'll get back just the category as a JSON string!

Conclusion

This article showed how easy it is to integrate state-of-the-art AI models into your applications with the OpenAI API, using Zero to help us manage the API key. While we only coded a single API method, it's easy to imagine how our API could be expanded to provide many more features, such as the ability to check if the total at the bottom of an invoice is accurate, or the ability to write a detailed description of an invoice item based on a 2-3 word summary of the service that was performed. To add each of these features, the only real work would be to craft the appropriate prompt to send to GPT, and then it would do all the hard work.


Other articles

A typewriter

Create a Lead Capture Form using the HubSpot API

Let's build a full stack web application that integrates with the API for HubSpot, a CRM platform that also offers sales, marketing, and CMS tools.

A tall office building

Build an Email Summary Bot using Claude AI, Gmail, and Slack (Part 1)

Integrate with the Gmail Push Notifications API to enable your app to intelligently respond to new emails.

Secure your secrets

Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.