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
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.
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.
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:
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:
-
Create a new directory called
payment-categorization-api
for your project. -
Enter the directory with
cd payment-categorization-api
. -
Install Express and TypeScript:
Terminalnpm install express @types/express typescript
This will automatically create a
package.json
for you. -
Create a
tsconfig.json
with the following settings.tsconfig.json{ "compilerOptions": { "target": "es2022", "lib": ["es2022"], "module": "ESNext", "moduleResolution": "node16", "esModuleInterop": true, "isolatedModules": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "dist" }, "include": ["src"] }
-
Create a
src
directory and add an emptymain.ts
file to it. This is the entrypoint to our application. -
Add
"type": "module"
to thepackage.json
to tell Node that our JavaScript files are ES Modules, not CommonJS modules. -
Add a
start
script topackage.json
that compiles our code using the TypeScript compiler and then runs it with Node:package.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:
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:
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.
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 ofGET
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, butPOST
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:
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:
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:
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:
- 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.
- Use
zero({ ... }).fetch()
to exchange our Zero token for the OpenAI secret. This expects the Zero token to passed in via an environment variable namedZERO_TOKEN
. This line of code will cause the Zero SDK to make a request to the Zero GraphQL API . - Create a
Configuration
object using the OpenAI API key we got from Zero. - Create an
OpenAIApi
API client using theConfiguration
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:
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.
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:
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:
{
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
:
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:
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
Add Error Monitoring to an Express.js App with Rollbar
Stay on top of errors in your Node.js apps to find and fix bugs faster.
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.
Secure your secrets
Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.