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
- 
stripFormattingremoves 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 - formattedTextback 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)