Selling gift cards with Commerce Layer.
Our simple page is a Next.js application and we are going to use Commerce Layer React Components and JS SDK to add all the needed transactional features. We will also make use of Commerce Layer hosted Checkout to place the order.
Setting up our project
Fork this repository in a local folder (in my case this would be giftcard-tutorial
). In Visual Studio Code the result will look like this:
The bulk of our web application lives in src/app/page.tsx
. Let’s start setting up our project by adding some libraries, namely:
- Commerce Layer JS SDK
- Commerce Layer JS Auth
- Commerce Layer React Components
giftcard-tutorial % npm i @commercelayer/sdk @commercelayer/js-auth @commercelayer/react-components
added 64 packages, and audited 405 packages in 17s
146 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Setting up Cloudinary as allowed source for images
We are storing the AI generated designs on Cloudinary (you can use whatever service you want). To do so, beside uploading the images on the service, we need to update Next.js configuration to allow an external origin for images:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
domains: ["res.cloudinary.com"],
},
};
export default nextConfig;
Now that we have all we need let’s start adding commerce capabilities to our simple page!
Adding Commerce Layer functionalities
The first thing to do is to wrap our application with the CommerceLayer
React component. The presence of this component is mandatory in order for all the other components that we are going to use to work properly.
Update src/app/layout.tsx
as follows:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { CommerceLayer } from "@commercelayer/react-components";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<CommerceLayer accessToken={<YOUR TOKEN HERE>}>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
</CommerceLayer>
);
}
As you see we have a placeholder for the token. Let’s provide a function that returns a sales channel token that can be used to purchase a gift card.
First, let’s create lib
folder under src
and add a commercelayer.ts
file to it, this will contain all the helpers we’ll need throughout the implementation of the tutorial.
src
+-app
+-lib
+-commercelayer.ts
Inside our helper file let’s add a function to retrieve a token using the js-auth
library. Let give a look at the code:
import { authenticate } from '@commercelayer/js-auth'
const CL_CLIENT_ID = process.env.NEXT_PUBLIC_CL_CLIENT_ID
if (!CL_CLIENT_ID) {
throw new Error('NEXT_PUBLIC_CL_CLIENT_ID is not defined in the environment variables')
}
interface TokenData {
accessToken: string
expiresAt: number
}
let tokenData: TokenData | null = null
export async function getValidToken() {
const auth = await authenticate('client_credentials', {
clientId: CL_CLIENT_ID!,
scope: 'market:code:us',
})
return auth.accessToken
}
Every time the page is loaded the getValidToken()
function will return a fresh token using the standard Commerce Layer OAuth 2.0 flow.
Please also notice that our helper file makes use of an environment variable to store the client ID, needed to get a sales channel token:
NEXT_PUBLIC_CL_CLIENT_ID=y0UR-cL13Nt-1D-Qd4-jd7_F_Gk
Now that we have a way to get a token, let’s pass it to our application:
import { getValidToken } from "@/lib/commercelayer";
...
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<CommerceLayer accessToken={await getValidToken()}>
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
</CommerceLayer>
);
}
From this point on our frontend is "connected" to Commerce Layer!
Following the theory explained in the first part of this guide, we know that a gift card can be created on the fly using the /api/gift_cards
endpoint.
The flow will then be:
- A customer lands on the page.
- The customer personalizes the gift card by setting all the attributes found in the page form.
- A gift card is created in Commerce Layer.
- The customer proceeds to payment and buys the gift card.
Creating the gift card
Let’s see how to create the gift card in Commerce Layer using the page form data.
As the customer inputs data in the form we store everything using state variables:
const [amount, setAmount] = useState<number | "">(presetAmounts[0]);
const [customAmount, setCustomAmount] = useState("");
const [selectedDesign, setSelectedDesign] = useState(designs[0].id);
const [recipient, setRecipient] = useState("");
const [message, setMessage] = useState("");
So what we are going to do is basically triggering gift card creation when the customer clicks on the Purchase button using the data stored in the state variables above.
In our page.tsx
we arleady have an handler for this event:
const handlePurchase = (e: React.FormEvent) => {
e.preventDefault();
// Placeholder: Add purchase logic here
alert("Gift card purchased! (Demo)");
};
What we are going to do is replacing the placeholder with the logic to:
- create the gift card
- redirecting to checkout page
Let’s start adding to our helper file commercelayer.ts
two things:
- a new interface
GiftCardData
- a function
createGiftCard
that takes in input gift card data and an SDK client
...
export interface giftCardData {
amount: number | string
design: string
recipientEmail: string
message: string
}
export async function createGiftCard(data: GiftCardData, client: CommerceLayerClient|undefined) {
if (!client) {
throw new Error("CommerceLayer client is not defined");
}
return client.gift_cards.create({
rechargeable: false,
single_use: false,
balance_cents: typeof data.amount === 'number' ? data.amount * 100 : parseFloat(data.amount) * 100,
image_url: data.design,
recipient_email: data.recipientEmail,
metadata:{
message: data.message
}
})
}
Our function just wraps an sdk call to our gift_card
endpoint passing the data collected so far.
Also it sets the gift card as not rechargeable and allowed for multiple use.
It’s time to add this function to the handler of the Purchase button:
"use client";
import { createGiftCard, GiftCardData } from "@/lib/commercelayer";
...
import { useCommerceLayer } from "@commercelayer/react-components";
const presetAmounts = [25, 50, 100, 200];
const designs = [
{ id: 1, name: "Confetti", image: "https://res.cloudinary.com/dvbqso85l/image/upload/v1751384150/confetti_u885gd.png" },
{ id: 2, name: "Birthday", image: "https://res.cloudinary.com/dvbqso85l/image/upload/v1751384150/classic_jqxrys.png" },
{ id: 3, name: "Classic", image: "https://res.cloudinary.com/dvbqso85l/image/upload/v1751384150/birthday_qtj5zm.png" },
];
...
const handlePurchase = async (e: React.FormEvent) => {
e.preventDefault();
try {
const giftCardData: GiftCardData = {
amount: (amount === "" ? parseFloat(customAmount) : amount) ,
design: designs.find(d => d.id === selectedDesign)?.image || "",
recipientEmail: recipient,
message: message,
}
const giftCard = await createGiftCard(giftCardData, sdkClient())
alert("Gift card purchased! " + giftCard.id);
} catch (error) {
console.error("Error creating gift card:", error);
alert("An error occurred while processing your purchase. Please try again.");
return;
}
};
...
Let’s break down the handlePurchase
function:
- First of all, we create an object that contains all the form-collected gift card details.
- The amount is either the amount selected from the pre populated options or a custom amount if the customer entered a random number.
- All the rest is pretty straight forward, with the only exception of the design where we basically extract from the design collection the one identified by the selected ID.
- Once done we pass the object and a client to our helper function (note that we used the hook
useCommerceLayer()
to retrieve an instance of the SDK given the current token). - For the moment we just display an alert with a message containing the gift card ID if everything went fine or display an error otherwise.
Once edited the file let’s save and test our application:
Boom! When we press Purchase, our gift card creation logic is triggered and the gift card is created in Commerce Layer as we can see trying to fetch the gift card by ID (we'll use our CLI to do that):
~ % cl get gift_card/rBgKUZzNqn
{
id: 'rBgKUZzNqn',
type: 'gift_cards',
status: 'draft',
code: 'bcd42606-fbfb-451d-b4dc-e58d47d2d2d7',
currency_code: 'USD',
initial_balance_cents: 20000,
initial_balance_float: 200,
formatted_initial_balance: '$200.00',
balance_cents: 20000,
balance_float: 200,
formatted_balance: '$200.00',
balance_max_cents: null,
balance_max_float: null,
formatted_balance_max: null,
balance_log: [ { datetime: '2025-07-01T15:44:28.469Z', balance_change_cents: 20000 } ],
usage_log: {},
single_use: false,
rechargeable: false,
distribute_discount: true,
image_url: 'https://res.cloudinary.com/dvbqso85l/image/upload/v1751384150/confetti_u885gd.png',
expires_at: null,
recipient_email: 'fabrizio@commercelayer.io',
created_at: '2025-07-01T15:44:28.469Z',
updated_at: '2025-07-01T15:44:28.469Z',
reference: null,
reference_origin: null,
metadata: { message: 'Compliments of Commerce Layer!' }
}
Purchasing the gift card
The next step is purchasing our gift card. This step can be further broken down in three operations:
- creating an order
- creating a line item for the gift card
- redirect the customer to the hosted checkout to purchase it
Let's start by adding a new helper function to commercelayer.ts
:
...
export async function addGiftCardToOrder(giftCard:string, client: CommerceLayerClient|undefined): Promise<string> {
if (!client) {
throw new Error("CommerceLayer client is not defined");
}
const order = await client.orders.create({})
client.line_items.create({
quantity: 1,
item: client.gift_cards.relationship(giftCard),
order: client.orders.relationship(order.id),
})
return order.id
}
...
The function, that takes as input the newly created gift card, wraps two operations, corresponding to the first two step listed above:
- creating a new order
- creating a line item for the gift card
This function of course is not ideal for real applications as it’s a bit opinionated (we only add gift cards to orders and we create a new order each time we create a gift card), but can work well in our example. Using the SDK we create an empty order, and right after we add a line item of type gift_cards
(the line item's type is inferred by the item
relationship) and we return the order ID.
To redirect the customer to the hosted checkout page we need to generate the checkout link. Let’s add a function that does this according to the documentation:
const CL_CLIENT_ID = process.env.NEXT_PUBLIC_CL_CLIENT_ID
const CL_ORG_SLUG = process.env.NEXT_PUBLIC_CL_ORG_SLUG
if (!CL_CLIENT_ID || !CL_ORG_SLUG) {
throw new Error('NEXT_PUBLIC_CL_CLIENT_ID or NEXT_PUBLIC_CL_ORG_SLUG is not defined in the environment variables')
}
...
export async function getCheckoutLink(orderId: string){
return `https://${CL_ORG_SLUG}.commercelayer.app/checkout/${orderId}?accessToken=${await getValidToken()}`
}
The function needs an additional environment variable to store the Commerce Layer organization slug needed to build the hosted chekout URL:
NEXT_PUBLIC_CL_CLIENT_ID=y0UR-cL13Nt-1D-Qd4-jd7_F_Gk
NEXT_PUBLIC_CL_ORG_SLUG=your-org-slug
Now that we have all the pieces needed in order to create an order, add the gift card to it, and get a checkout link, let’s assemble everything in the application code inside the handler of the purchase button:
...
const handlePurchase = async (e: React.FormEvent) => {
e.preventDefault();
try {
...
const giftCard = await createGiftCard(giftCardData, sdkClient())
const order = await addGiftCardToOrder(giftCard.id, sdkClient())
const checkoutLink = await getCheckoutLink(order)
window.location.href = checkoutLink;
} catch (error) {
console.error("Error creating gift card:", error);
alert("An error occurred while processing your purchase. Please try again.");
return;
}
};
...
There we go! Now when the pressing the Purchase button a redirection to the hosted checkout page happens:
Once the order is placed, the newly purchased gift card will show up in the Dashboard Gift cards app as inactive, according to the lifecycle we explained in the previous article:
Integration with Zapier for gift card activation
If the creation and purchase of gift cards is entirely managed by Commerce Layer APIs, the activation might require some integration. It's always is possible to activate gift cards straight from the Dashboard Gift card app, but you probably don’t want your customer service to be flooded with phone calls asking for that.
The real flow should follow the steps below:
To implement this flow, this is what we are going to do:
- Set up a zap in Zapier that uses one of the triggers supported by the Commerce Layer Zapier app to start a workflow every time a gift card is purchased.
- Link this to one of actions supported by the Commerce Layer Zapier app to sends an email that notifies the recipient of the gift.
- Implement (still in Zapier) the endpoint the recipient will click to activate the gift card.
- Activate the gift card using (again) the Zapier app.
First of all let’s create a set of integration API credentials from the Commerce Layer Dashboard (Developers > API Crendentials):
Setting up the trigger
Now we need to create in Zapier a new zap and set up a new trigger using the Commerce Layer app. Give a meaningful name to the trigger, and select New Gift Card Event among the available events supported by our app:
When asked for the account create one as following:
Hit Continue and in the next panel select purchase as the specific event we want to be notified:
Hit Continue and test your trigger. If everything went well you’ll see test data showing in the panel (remember to purchase a gift card first):
The trigger is now set up.
Setting up the action
The next step is configuring the notification action triggered by the gift card purchase event. For simplicity here we’ll just use the GMail Zapier app and send a basic email to the recipient of the gift card. Once added the next step in the zap pipeline, select the GMail app and fill the fields according to your account information:
In the Configure panel we can define how the email notification will look like:
Let’s see in details what we added:
- To: is the destination of the message, it contains the recipient of the gift card.
- Subject: the subject of the message.
- Body type: we chose HTML.
- Body: we used the placeholder variables available from the previous step to populate the body of the message (in our case, the original message defined in the gift card creation page, the code of the gift card, and finally the activation link, that contains the internal ID of the gift card to be activated — we’ll go back on this in a short while, when we will build the callback for the activation link).
Also here you can hit Continue and test with the test data. If everything goes well you should get an email in your inbox!
Implementing the activation link
We now need to implementation of the activation link. We are going to use Zapier for this part as well. Let’s publish the zap we worked on so far and create a new one that we’ll call "Gift Card Activation":
The trigger in this case will be the Webhook by Zapier app, that will expose an endpoint that will trigger the execution pipeline:
Activating the gift card
The last step is close the pipeline by using again the Commerce Layer Zapier app and specify as the event Activate Gift Card:
The activation action requires the ID of the gift card to be activated. We’ll be passing it as a query string parameter to the URL we generated before:
You can now save and publish the zap. If everything works correctly, when customers hit the activation link in the email they should be presented with something similar to the screenshot below:
But let’s see what happened to our gift card in the Dashboard:
The gift card is now active and can be used as a payment method!
Conclusion
In this tutorial, we walked through the full lifecycle of a Commerce Layer gift card — from personalization and creation to purchase and activation. We integrated Commerce Layer’s APIs and React components into a simple Next.js app, using the hosted checkout for payment and Zapier for automating the activation flow.
While the implementation presented is simplified for demonstration purposes, it provides a solid foundation for building a real-world gift card experience. By combining Commerce Layer’s robust ecommerce backend with flexible frontend tools and third-party automation platforms, developers can deliver personalized, scalable, and automated gifting solutions.
Whether you plan to enhance the design, manage activation internally, or support more advanced use cases like scheduling and rechargeability, the core concepts remain the same — clean API integration, smart token handling, and event-driven workflows.
Happy coding — and happy gifting!