Summary
WhatsApp imposes a 24-hour customer-service window during which you can send any message; outside of it, only template messages (unformatted) are allowed. Goalmatic.io needs to let users send richly formatted content (articles, poems, etc.), so we built a two-step workaround:
- Template Send: Strip formatting and send via a WhatsApp template.
- On-Click Upgrade: Include a “Get Formatted Text” button in the template. When the user clicks, the 24-hour window reopens, allowing us to send the original, fully formatted message.
Below is an end-to-end example in Node.js using the WhatsApp Cloud API.
The Challenge
WhatsApp’s customer-service window rules mean:
- Within 24 hours of a user message, you can send any content (text, media, formatted).
- After 24 hours, only template messages (unformatted, pre-approved) are allowed.
But Goalmatic users want formatting—bold, italics, lists, links—to enrich their content. If you try to include Markdown or HTML in a template payload, WhatsApp returns a 400 error. How do we let users see formatted content when they click outside the 24-hour window?
🛠️ Two-Part Workaround
- Send the Template Strip all Markdown/HTML. Use a generic “Your content is here” template and include a button:
type dataType = {
message: string;
recipientNumber: string;
uniqueTemplateMessageId: string;
}
export const goalmatic_whatsapp_workflow_template = (data: dataType) => {
return JSON.stringify({
'messaging_product': 'whatsapp',
'recipient_type': 'individual',
'to': data.recipientNumber,
'type': 'template',
'template': {
'name': 'workflow_template',
'language': {
'code': 'en',
},
'components': [
{
'type': 'header',
'parameters': [
{
'type': 'image',
'image': {
'link': 'https://goalmatic.io/hero/workflow.png',
},
},
],
},
{
'type': 'body',
'parameters': [
{
'type': 'text',
'text': data.message,
},
],
},
{
'type': 'button',
"index": "0",
"sub_type": "quick_reply",
'parameters': [
{
"type": "payload",
"payload": data.uniqueTemplateMessageId
}
],
},
],
},
})
}
- Handle the Click When the user taps “Get Formatted Text,” WhatsApp opens a conversation thread (re-opening the 24-hour window). Your webhook sees this click (or an inbound URL request) and responds with the full formatted content using a standard text API call.
Code Snippet: Stripping Formatting & Sending Both Messages
import express from 'express';
import bodyParser from 'body-parser';
import fetch from 'node-fetch';
// Helpers
/**
* Strip markdown/HTML tags to produce unformatted text
* @param {string} input
* @returns {string}
*/
function stripFormatting(input) {
// Simple removal of markdown/HTML; adjust regex as needed
return input
.replace(/<\/?[^>]+(>|$)/g, '') // remove HTML tags
.replace(/(\*|_|~|`){1,3}/g, '') // remove markdown **, __, ~~ , `
.trim();
}
/**
* Send a WhatsApp Cloud API message
* @param {string} payload
*/
async function sendWhatsApp(payload) {
const token = process.env.WA_CLOUD_TOKEN;
const phoneNumberId = process.env.WA_PHONE_NUMBER_ID;
const res = await fetch(
`https://graph.facebook.com/v15.0/${phoneNumberId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
);
if (!res.ok) {
const err = await res.text();
console.error('WhatsApp API error:', err);
}
}
// Express App
const app = express();
app.use(bodyParser.json());
app.post('/send', async (req, res) => {
const { to, formattedText } = req.body;
const unformattedText = stripFormatting(formattedText);
// 1️⃣ Send unformatted template with button
await sendWhatsApp({
to,
type: 'template',
template: {
name: 'get_formatted_text',
language: { code: 'en_US' },
components: [
{
type: 'BODY',
parameters: [{ type: 'text', text: unformattedText }]
},
{
type: 'BUTTONS',
buttons: [
{
type: 'url',
url: `https://your-domain.com/fetch?to=${encodeURIComponent(to)}&msg=${encodeURIComponent(formattedText)}`,
title: 'Get Formatted Text'
}
]
}
]
}
});
res.sendStatus(200);
});
app.get('/fetch', async (req, res) => {
// 2️⃣ User clicked button; send formatted text
const { to, msg } = req.query;
await sendWhatsApp({
to,
type: 'text',
text: { body: msg }
});
res.send('<h1>Formatted message sent! ✅</h1>');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));
How It Works
-
stripFormatting
removes Markdown and HTML so the template body is plain text. POST /send
- Receives the user’s destination number and full formatted text.
-
Sends a WhatsApp template message with a URL-button “Get Formatted Text.”
GET /fetch
Triggered when the user clicks the button (opening the 24-hour window).
Sends the original
formattedText
back as a non-template text message, preserving all formatting.
Next Steps & Tips
- Template Approval: You must pre-approve your “get_formatted_text” template with Facebook Business Manager.
- Security: Sign or encrypt the URL parameters so users can’t tamper with message text.
- Rich Media: You can adapt this pattern to send images, documents, or even interactive lists once the window is open.
- UX: Customize the “Get Formatted Text” button title or use a quick-reply button to keep users inside WhatsApp.
Have you built something similar or run into other messaging-platform quirks? Let me know in the comments!
Top comments (0)