WhatsApp Channel Conformance Spec
Conformance tests for the Norq WhatsApp compiler. The WhatsApp compiler takes
Markdown input (with optional YAML frontmatter) and produces a WhatsAppOutput
with message_type and payload fields.
Each example block specifies input markdown and one or more expected output
fragments (substrings that must appear in the serialized JSON payload).
Format:
- Opening fence: 32 backticks followed by
example channel=whatsapp - Input markdown (first section, before the first
.separator) .separator line- One or more expected fragments, each separated by
.lines - Closing fence: 32 backticks
The test runner serializes WhatsAppOutput.payload using serde_json::to_string
and checks that every expected fragment appears as a substring of that JSON
string.
Message Types
The WhatsApp compiler selects one of four message types based on content:
| Condition | message_type |
Payload shape |
|---|---|---|
:::action with links present |
interactive |
{type, body, action, [footer]} |
:::hero or :::media with image |
image |
{link, [caption]} |
mode: template in frontmatter |
template |
{type, template, variable_mapping} |
| Otherwise | text |
{body} |
Interactive takes priority over image; image takes priority over text.
Text Messages
Plain paragraph
A plain paragraph compiles to a text message with a body string field.
Hello from WhatsApp.
.
"body":"Hello from WhatsApp."Multiple paragraphs joined with newline
Multiple paragraphs are joined with \n in the body string. The JSON payload
encodes the newline as \n.
First line.
Second line.
.
First line.
.
Second line.WhatsApp Text Formatting
WhatsApp uses its own markup language rather than HTML. The compiler converts Markdown inline formatting to WhatsApp-native equivalents.
Bold → *text*
Markdown bold (**text** or __text__) maps to WhatsApp bold: single
asterisks wrapping the text (*text*).
Your order **#12345** is ready.
.
*#12345*Italic → _text_
Markdown italic (*text* or _text_) maps to WhatsApp italic: single
underscores wrapping the text (_text_).
Please _confirm_ your appointment.
.
_confirm_Strikethrough → ~text~
Markdown strikethrough (~~text~~) maps to WhatsApp strikethrough: single
tildes wrapping the text (~text~).
~~Old price~~ New price
.
~Old price~Inline code → ```code```
Markdown inline code (`code`) maps to WhatsApp monospace: triple
backticks wrapping the text.
Run `git pull` to update.
.
```git pull```Links render as link text only
WhatsApp does not support hyperlinks in message body text. Inline links render
as their visible text; the URL is discarded. Use :::action to surface URLs
as interactive buttons.
Visit [our website](https://example.com) for more.
.
Visit our website for more.Headings
Headings render as WhatsApp bold text (*text*). WhatsApp has no heading
concept, so all heading levels map to the same bold treatment.
# Order Confirmed
.
*Order Confirmed*## Shipment Update
Your package is on the way.
.
*Shipment Update*
.
Your package is on the way.:::header Directive
:::header content is wrapped in WhatsApp bold (*text*) and prepended to
the body. It is treated as a bold section header, not as a push title.
:::header
Order Update
:::
Your order is on the way.
.
*Order Update*
.
Your order is on the way.:::highlight Directive
:::highlight content is wrapped in WhatsApp bold (*text*) and included in
the body. Unlike SMS which uses a ⭐ prefix, WhatsApp uses bold formatting.
:::highlight
Special announcement
:::
.
*Special announcement*:::callout Directive
:::callout content is prefixed with the ⚠ warning emoji (U+26A0) and
included in the body.
:::callout
Your account is at risk
:::
.
⚠ Your account is at risk:::fields Directive
:::fields renders each paragraph child as a *Key:* Value line. The key
(text before the first colon) is wrapped in WhatsApp bold; the value (text
after the colon) is plain.
:::fields
Order: ORD-123
Status: Shipped
:::
.
*Order:* ORD-123
.
*Status:* Shipped:::columns Directive
:::columns stacks each :::col child's content as sequential lines in the
body. The column layout has no visual meaning in WhatsApp.
:::columns
::: col
Left content
:::
::: col
Right content
:::
:::
.
Left content
.
Right contentBlockquote
Blockquote paragraph children are prefixed with > in the WhatsApp body.
> This is an important note.
.
> This is an important note.Lists
Unordered list
Unordered list items are prefixed with - and joined with \n.
- Item one
- Item two
.
- Item one
.
- Item twoOrdered list
Ordered list items are prefixed with N. (1-based index).
1. First step
2. Second step
.
1. First step
.
2. Second stepTask list items
Checked task list items are prefixed with the ✅ emoji (U+2705) and
unchecked items with ☐ (U+2610), after the list bullet.
- [x] Account created
- [ ] Profile completed
.
✅ Account created
.
☐ Profile completedInteractive Messages (Buttons)
When :::action contains one or more links, the compiler produces an
interactive message rather than a text message. WhatsApp interactive
messages have a type, body, and action.buttons array. An optional
footer field is included if :::footer is present.
Basic interactive message
Your order has shipped.
:::action
[Track Order](https://example.com/track)
:::
.
"type":"button"
.
"body":{"text":
.
Track OrderButton structure
Each button in the action.buttons array has type: "reply", a unique
reply.id (e.g., "btn-0"), and a reply.title with the link text.
Ready to start?
:::action
[Get Started](https://example.com/start)
:::
.
"type":"reply"
.
"id":"btn-0"
.
"title":"Get Started"Maximum 3 buttons
WhatsApp allows at most 3 reply buttons. When :::action contains more than 3
links, only the first 3 are emitted.
:::action
[Btn1](https://example.com/1) [Btn2](https://example.com/2) [Btn3](https://example.com/3) [Btn4](https://example.com/4)
:::
.
"btn-0"
.
"btn-1"
.
"btn-2"Footer in interactive messages
:::footer content is extracted as plain text and placed in a footer.text
field in the interactive payload.
Message body.
:::action
[Confirm](https://example.com/confirm)
:::
:::footer
Powered by SuprSend
:::
.
"footer":{"text":"Powered by SuprSend"}Image Messages
When :::hero or :::media contains an image and no :::action buttons are
present, the compiler produces an image message with a link field. If body
text is present it is included as caption.
:::hero

:::
.
"link":"https://example.com/banner.jpg":::hero

:::
Check out our latest collection.
.
"link":"https://example.com/banner.jpg"
.
"caption":"Check out our latest collection."Emoji Shortcodes
Emoji shortcodes (e.g., :wave:, :tada:) are expanded to Unicode in the
final body string. Expansion runs after all compilation on the trimmed text.
Hello :wave: welcome!
.
Hello 👋 welcome!:::footer and :::action Excluded from Body Text
:::footer and :::action content is excluded from the body text field. They
are handled separately as button metadata and footer text.
Your shipment is on its way.
:::footer
Terms apply
:::
.
Your shipment is on its way.Template Mode
When frontmatter contains mode: template, the compiler produces a
template message type for the WhatsApp Business API. Template mode requires
template_name and template_language frontmatter fields.
Template payloads contain a template object with name, language.code,
and components (positional parameter arrays), plus a variable_mapping that
maps variable positions to data paths.
:::each and :::list directives are forbidden in template mode.
Basic template payload
---
mode: template
template_name: order_update
template_language: en
---
Your order is ready.
.
"type":"template"
.
"name":"order_update"
.
"code":"en"Dynamic Value Escaping
Expression values containing WhatsApp formatting characters (*, _, ~,
`) are escaped with a backslash to prevent unintended formatting. Static
text (non-expression) is not escaped.
With null data, expression paths resolve to empty strings and produce no output. The static surrounding text is unaffected.
Hello there!
.
Hello there!