Courier Elemental lets you define notification content as JSON instead of using the visual template designer. This is useful when your content is generated dynamically, managed in code, or needs to differ per channel.
This tutorial walks through building a real notification end-to-end: starting with a simple message, then layering in channel-specific content, conditional rendering, and dynamic lists.
Prerequisites
Send a Simple Elemental Message
The fastest way to use Elemental is the sugar syntax: just title and body. Courier automatically converts this into the full Elemental format behind the scenes.
Send with title and body
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"to": { "email": "jane@example.com" },
"content": {
"title": "Your order has shipped",
"body": "Hi {{name}}, your order #{{order_id}} is on its way. Expected delivery: {{delivery_date}}."
},
"data": {
"name": "Jane",
"order_id": "ORD-9042",
"delivery_date": "Feb 12, 2026"
}
}
}'
This sends an email with the subject “Your order has shipped” and the body text populated with your template variables. No stored template required.Verify delivery
Check the Message Logs in the Courier dashboard to confirm the message was delivered and see the rendered output.
The sugar syntax is ideal for simple messages. For multi-element layouts, channel-specific content, or dynamic logic, use the full Elemental format below.
Full Elemental gives you complete control over the notification structure. Every template requires a version field ("2022-01-01") and an elements array.
Build a structured notification
This example creates an order confirmation with a heading, body text, and a call-to-action button.curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"to": { "email": "jane@example.com" },
"content": {
"version": "2022-01-01",
"elements": [
{
"type": "meta",
"title": "Order #{{order_id}} Confirmed"
},
{
"type": "text",
"content": "Hi {{name}}, your order has been confirmed and is being prepared."
},
{
"type": "action",
"content": "Track Your Order",
"href": "https://example.com/orders/{{order_id}}"
},
{
"type": "divider"
},
{
"type": "text",
"content": "Questions? Reply to this email or visit our help center.",
"text_style": "subtext"
}
]
},
"data": {
"name": "Jane",
"order_id": "ORD-9042"
}
}
}'
Element Types at a Glance
| Element | Purpose | Key properties |
|---|
meta | Sets the email subject / push title | title |
text | Body text with optional formatting | content, text_style, format, align |
action | Button or link | content, href, style (button or link) |
image | Inline image | src, alt_text, width, href |
divider | Horizontal rule | color |
quote | Blockquote text | content, border_color |
html | Raw HTML (email only) | content |
group | Container for conditional/loop logic | elements, if, loop |
channel | Channel-specific content branch | channel, elements, raw |
columns | Multi-column layout | elements (array of column nodes) |
For full property details, see the Elements Reference.
Customize Content Per Channel
Use channel elements to send different content to different channels from a single API call. This is one of Elemental’s most powerful features.
{
"version": "2022-01-01",
"elements": [
{
"type": "channel",
"channel": "email",
"elements": [
{ "type": "meta", "title": "Order #{{order_id}} Confirmed" },
{ "type": "text", "content": "Hi {{name}}, here are your order details..." },
{ "type": "image", "src": "{{product_image}}", "alt_text": "{{product_name}}" },
{ "type": "action", "content": "Track Order", "href": "{{tracking_url}}" }
]
},
{
"type": "channel",
"channel": "sms",
"elements": [
{ "type": "text", "content": "Order #{{order_id}} confirmed! Track: {{tracking_url}}" }
]
},
{
"type": "channel",
"channel": "push",
"elements": [
{ "type": "meta", "title": "Order Confirmed" },
{ "type": "text", "content": "Your order #{{order_id}} is on its way." }
]
}
]
}
You can also use the channels property on individual elements to show or hide them by channel without wrapping in a channel block:
{
"type": "text",
"content": "This only appears in email and push",
"channels": ["email", "push"]
}
Add Conditional Logic
Use the if property on any element to conditionally render it based on your data.
{
"version": "2022-01-01",
"elements": [
{
"type": "text",
"content": "Welcome back, {{name}}!"
},
{
"type": "text",
"content": "As a premium member, you have early access to new features.",
"if": "data.tier === 'premium'"
},
{
"type": "action",
"content": "Upgrade to Premium",
"href": "https://example.com/upgrade",
"if": "data.tier !== 'premium'"
}
]
}
The if expression is evaluated as JavaScript against the data object you pass in the send call.
Render Dynamic Lists
The loop property iterates over an array in your data and renders the element once per item.
{
"version": "2022-01-01",
"elements": [
{
"type": "text",
"content": "Your recent orders:",
"text_style": "h2"
},
{
"type": "group",
"loop": "data.orders",
"elements": [
{
"type": "text",
"content": "**{{$.item.name}}** ; {{$.item.quantity}}x ; ${{$.item.price}}"
}
]
},
{
"type": "divider"
},
{
"type": "text",
"content": "Total: ${{total}}"
}
]
}
Inside a loop, {{$.item}} refers to the current item and {{$.index}} gives the zero-based index.
The format property on text elements supports "markdown" for bold, italic, and link rendering. Use double asterisks (**bold**) or single asterisks (*italic*) in your content strings.
Send to a Stored User Profile
Instead of specifying an email address directly, you can send to a user by their profile ID. Courier looks up their contact details from the stored profile.
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"to": { "user_id": "user_123" },
"content": {
"version": "2022-01-01",
"elements": [
{ "type": "meta", "title": "Weekly Summary" },
{ "type": "text", "content": "Here is your weekly activity summary, {{name}}." }
]
},
"data": { "name": "Jane" }
}
}'
What’s Next