schwamm-builder
schwamm-builder
Fluent message builders for Baileys (@whiskeysockets/baileys).
Chainable helpers for interactive buttons, lists, carousels, payments, products, media, and rich AI-style messages.
npm i @whiskeysockets/baileys schwamm-builder
| Node.js | 18+ |
| Peer dependency | @whiskeysockets/baileys (install in your bot; this package does not ship Baileys) |
| License | MIT |
Your bot owns the WhatsApp socket. schwamm-builder only builds payloads and sends them through that socket.
Quick start
const Builder = require('schwamm-builder');
// sock = Baileys socket
// jid = chat id string (or object with id / chat / remoteJid)
await Builder.other()
.xText('Hello')
.send(sock, jid);
| Entry | Purpose |
|---|---|
Builder.interactive() |
Buttons, lists, nested lists, offers, carousel |
Builder.payment() |
Payment request, order, product |
Builder.other() |
Text, image, video, sticker |
Builder.rich() |
Rich / GenAI-style sections (tables, posts, reels, …) |
.send(sock, jid)
Interactive
await Builder.interactive()
.xText('Choose an option')
.xFooter('Schwamm Bot')
.xButton('Option 1', 'opt1')
.xButton('Option 2', 'opt2', 'REVIEW')
.xUrlButton('Website', 'https://example.com', { icon: 'REVIEW' })
.xCallButton('Call', '491234567890')
.xCopyButton('Copy code', 'SAVE20')
.send(sock, jid);
Button types
| Method | Type |
|---|---|
.xButton(text, id, icon?) |
Quick reply |
.xUrlButton(text, url, { icon, useWebview }) |
Open URL |
.xCallButton(text, phone, icon?) |
Call |
.xCopyButton(text, code, icon?) |
Copy |
.xReminder(text, id?, icon?) |
Reminder |
.xLocation() |
Request location |
.xList(title, sections, icon?) |
Single-select list |
.xListRows(title, rows, sectionTitle?, icon?) |
List helper |
List
.xList('Options', [
{
title: 'Menu',
rows: [
{ title: 'Profile', id: 'profile', description: 'Your profile' },
{ title: 'Help', id: 'help', description: 'Support' }
]
}
], 'REVIEW')
Nested list (bottom sheet)
Call .xNestedList(buttonTitle, listTitle) first, then one or more .xList(...).
await Builder.interactive()
.xText('Anti system')
.xFooter('Bot')
.xNestedList('Options', 'Anti')
.xList('Antilink', [{
title: 'Antilink',
rows: [
{ title: 'On', id: 'al_on' },
{ title: 'Off', id: 'al_off' }
]
}], 'REVIEW')
.xList('Antispam', [{
title: 'Antispam',
rows: [{ title: 'On', id: 'as_on' }]
}], 'CALL')
.xButton('Close', 'close')
.send(sock, jid);
Offer
.xOffer('Limited offer', {
url: 'https://example.com/pay',
code: 'SAVE20',
expiration: Math.floor(Date.now() / 1000) + 3600
})
Image header
.xImage('https://example.com/banner.jpg')
.xTitle('Title')
Carousel
Each card needs an image. After .xCard(...), chain buttons or lists onto that card:
const img = 'https://example.com/card.jpg';
await Builder.interactive()
.xText('Shop')
.xFooter('Bot')
.xCard({ title: 'Item 1', text: 'Description', image: img })
.xButton('Buy', 'buy_1', 'REVIEW')
.xUrlButton('Shop', 'https://example.com')
.xCard({ title: 'Item 2', text: 'More', image: img })
.xList('Options', [{
title: 'Settings',
rows: [
{ title: 'Enable', id: 'on' },
{ title: 'Disable', id: 'off' }
]
}], 'REVIEW')
.send(sock, jid);
You can also pass buttons inside the card object, or use .xCards([...]).
Nested list on a card:
.xCard({ title: 'Anti', text: 'Settings', image: img })
.xNestedList('Options', 'Anti')
.xList('Antilink', [/* ... */])
.xList('Antispam', [/* ... */])
Payment
Note: Some payment UIs depend on region, account type, and WhatsApp client. Fake catalog / business IDs often show as “not supported”.
Request payment
await Builder.payment()
.xRequest({
amount: 25,
currency: 'EUR',
text: 'Please pay 25 EUR',
from: '0@s.whatsapp.net'
})
.send(sock, jid);
Order
await Builder.payment()
.xOrder({
text: 'Thanks for your order',
amount: 67,
currency: 'EUR',
itemCount: 1,
thumbnail: 'https://example.com/thumb.png' // URL or Buffer
})
.send(sock, jid);
URLs are loaded to a buffer automatically when needed.
Product
await Builder.payment()
.xProduct({
title: 'Premium',
description: 'One month access',
amount: 9.99,
currency: 'EUR',
image: 'https://example.com/product.png',
productId: 'sku-1',
retailerId: '',
businessOwnerJid: '0@s.whatsapp.net'
})
.send(sock, jid);
image is required (URL or Buffer).
Other (text / media)
await Builder.other().xText('Hello').send(sock, jid);
await Builder.other()
.xImage('https://example.com/pic.jpg', 'Caption')
.send(sock, jid);
await Builder.other()
.xVideo('https://example.com/vid.mp4', 'Caption')
.send(sock, jid);
Rich
Rich messages use unified GenAI-style sections. Rendering is client-dependent.
Text & media
await Builder.rich()
.xHeader('Title')
.xBody('Body text')
.xBanner('https://example.com/banner.png')
.xPicture('https://example.com/pic.png', { inline: true, width: 200, height: 200 })
.xPictures(['https://a.png', 'https://b.png'])
.xTip('Small tip text')
.xLink('Open site', 'https://example.com')
.xInfo('Disclaimer under the message')
.xFooter('Open', 'https://example.com')
.send(sock, jid);
Tables & code
.xTableV1('Users', [
['Name', 'Role'],
['Alice', 'Admin'],
['Bob', 'User']
])
.xTableV2('Users', [
['Name', 'Role'],
['Alice', 'Admin']
])
.xCodeV1('console.log("hi")', 'javascript')
.xCodeV2('console.log("hi")', 'javascript')
Actions & carousel widgets
.xActionRow('Menu', [
{ label: 'Start', toast: 'OK' },
{ label: 'Help', toast: 'Help' }
])
.xCarousel([
{ title: 'Card 1', ctas: [{ label: 'Go', toast: '…' }] },
{ title: 'Card 2', ctas: [{ label: 'More' }] }
])
Posts, reels, products
.xPosts([{
username: 'user',
title: 'Post',
thumbnail_url: 'https://…',
post_url: 'https://…',
is_verified: true,
likes_count: 10
}])
.xReels([{
username: 'creator',
videoUrl: 'https://…',
thumbnailUrl: 'https://…',
is_verified: true
}])
.xProduct({
title: 'Item',
brand: 'Brand',
price: '9.99 EUR',
product_url: 'https://…',
image_url: 'https://…'
})
.xProducts([/* same shape */])
Sources, citations, suggestions
.xSources([
{ name: 'Wikipedia', url: 'https://…', subtitle: 'Web', favicon: 'https://…' }
])
// or tuples: [favicon, url, name, subtitle?]
.xCitations([
{ title: 'Source 1', url: 'https://…', snippet: '…' }
])
.xSuggestions(['Next question', 'Explain more'])
Global flags (all builders)
Available on interactive, payment, other, and rich:
| Method | Effect |
|---|---|
.xAI(true) |
AI / bot label (private chats only) |
.xSecureMeta(true) |
Secure meta / business attributes |
.xForwarded(n) |
Forwarded style with score n (e.g. 1 or 7) |
.xWAStatus(text, type?, opts?) |
Looks like a reply to a WhatsApp status |
xForwarded and xWAStatus are independent:
- only
.xForwarded(1)→ forward only - only
.xWAStatus(...)→ status-quote only - both → merged
Status quote types
.xWAStatus('Reply text') // text
.xWAStatus('Reply', 'image')
.xWAStatus('Reply', 'video')
.xWAStatus('Reply', 'sticker')
.xWAStatus('Reply', 'stickerpack', 'Pack Name')
.xWAStatus('Reply', 'product', { title: 'Abo', price: 9.99, currency: 'EUR' })
.xWAStatus('Reply', 'order', { message: 'Under 1 item', itemCount: 1 })
Examples
await Builder.other().xText('Hi').xAI(true).send(sock, jid);
await Builder.interactive()
.xText('Menu')
.xButton('OK', 'ok')
.xForwarded(1)
.send(sock, jid);
await Builder.payment()
.xRequest({ amount: 5, currency: 'EUR', text: 'Pay' })
.xSecureMeta(true)
.send(sock, jid);
await Builder.other()
.xWAStatus('Hello', 'image')
.xForwarded(7)
.send(sock, jid);
await Builder.rich()
.xBody('Answer')
.xSuggestions(['More'])
.xAI(true)
.send(sock, jid);
Notes
.xAI(true)is intended for private chats (@s.whatsapp.net/@lid). Groups may throw.- AI label, secure meta, status icons, and rich sections depend on the WhatsApp client and Baileys version.
- Do not mix interactive + payment + rich in a single message. Use one builder per
.send(). Global flags may still be chained on any builder.
Bot command example
const Builder = require('schwamm-builder');
module.exports = {
name: 'menu',
async run(x, s) {
try {
await Builder.interactive()
.xText('Choose')
.xButton('A', 'a')
.xButton('B', 'b')
.send(x, s.chat);
} catch (err) {
await s.m.reply(String(err?.message || err));
}
}
};
x / sock is whatever your bot passes as the Baileys socket. The builder does not care about the variable name.
Handling button / list clicks
Clicks are not handled inside the builder. In your messages.upsert handler, read the selected id from interactive / list / native-flow responses and route it to your command logic (e.g. a selected handler).
Typical sources:
interactiveResponseMessagelistResponseMessagebuttonsResponseMessagenativeFlowResponseMessage(paramsJson→id)
License
MIT
Copyright (c) 2026 Schwamm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.