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)