Carousels work well for small catalogs: three to seven cards, each with an image and one action.
1. Prepare the data
const products = [
{
id: 'arabica-coffee',
name: 'Arabica coffee 250g',
price: '$12',
image: 'https://example.com/coffee.jpg',
},
{
id: 'brew-kit',
name: 'Manual brew kit',
price: '$34',
image: 'https://example.com/kit.jpg',
},
{
id: 'tumbler',
name: 'Tumbler 500ml',
price: '$17',
image: 'https://example.com/tumbler.jpg',
},
];
2. Upload images and build cards
import { Carousel, Toolkit } from './lib/MessageBuilder.js';
async function buildCards(sock, products) {
return Promise.all(
products.map(async (product) => {
const imageUrl = await Toolkit.resolveMedia(
sock,
product.image,
'image',
{ resolveUrl: true }
);
return {
header: {
hasMediaAttachment: true,
imageMessage: { url: imageUrl },
},
body: { text: product.name },
footer: { text: product.price },
nativeFlowMessage: {
buttons: [
{
name: 'quick_reply',
buttonParamsJson: JSON.stringify({
display_text: 'View product',
id: `product:${product.id}`,
}),
},
],
},
};
})
);
}
3. Send the carousel
async function sendCatalog(sock, jid) {
const cards = await buildCards(sock, products);
await new Carousel(sock)
.setBody('Recommended products for you')
.setFooter('Prices may change')
.addCard(cards)
.send(jid);
}
4. Handle the selection
const selectedId =
msg.message?.buttonsResponseMessage?.selectedButtonId;
if (selectedId?.startsWith('product:')) {
const productId = selectedId.split(':')[1];
await sendProductDetail(sock, msg.key.remoteJid, productId);
}
Notes
addCard()accepts one object or an array. Header media is required.Toolkit.resolveMedia(..., { resolveUrl: true })forces a reupload. For stable public CDN URLs, leave itfalseto save time.- Do not reuse one ID for multiple cards if you plan to edit them later. Carousel IDs live at the button level, not the card level.
- Test card counts on target devices. Too many cards force users to scroll a long way.
- If an image fails to load, the carousel may render without media. Provide a fallback image or skip that product.
Safe pattern
const cards = await buildCards(sock, products.slice(0, 7));
if (cards.length === 0) {
await sock.sendMessage(jid, { text: 'The catalog is empty.' });
return;
}
await new Carousel(sock).setBody('Catalog').addCard(cards).send(jid);