Sync from your repo
Keep your message content in version control and push it to Messy with one command. Edit templates as files, review them in pull requests, and sync on deploy.
Messy has a /sync endpoint that upserts your layouts, folders and templates in a single call. Point a small script at a folder of files and you get a repeatable, reviewable workflow: content lives next to your code, changes go through review, and a deploy step pushes the latest version live.
Rather than ship a library you have to adopt, we describe the format and the API, give you a short reference script, and provide a prompt you can hand to your own coding assistant to generate the same tool in whatever language your stack uses.
File layout
Put your content in a templates/ directory. Each .md file holds one or more templates; the folder a file sits in becomes its folder in Messy (nesting allowed).
templates/
layouts.yml # optional shared email wrappers
transactional/
order-confirmed.md
invoice.md
lifecycle/
welcome.md # a file may hold several templatesA template file is YAML frontmatter between two --- lines, followed by the body. Separate multiple templates in one file with a line containing only ===.
---
trigger: order_confirmed
name: Order confirmation
channel: email
subject: "Order {{ order_number }} confirmed"
layout: transactional
body_format: markdown
---
# Thanks, {{ name }}
Order **{{ order_number }}** totalling **{{ total }}** is on its way.
[Track your order]({{ tracking_url }})Layouts are an optional layouts.yml with a {{ content }} slot where each template body is dropped in:
- name: transactional
body: |
<div style="font-family: sans-serif; max-width: 560px; margin: 0 auto">
{{ content }}
<hr />
<small>Sent by Acme. {{base_url}}</small>
</div>Frontmatter fields
| Field | Type | Description |
|---|---|---|
triggerreq | string | Unique handle used by POST /messages/trigger. |
namereq | string | Human-readable name shown in the app. |
channel | string | email, sms, whatsapp or push. Defaults to email. |
subject | string | Email subject (Liquid allowed). |
preview | string | Preheader / preview text. |
body_format | string | markdown or html. Defaults to markdown. |
layout | string | Name of a layout (from layouts.yml) to wrap the body. |
{{base_url}} is handy for environment-specific links: replace it with your app URL before sending so the same files work across staging and production.The sync API
Parse your files into this shape and POST it. Authentication is the usual environment API key.
curl https://api.messy.sh/sync \
-H "Authorization: Bearer $MESSY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"layouts": [{ "name": "transactional", "body": "...{{ content }}..." }],
"templates": [{ "trigger": "order_confirmed", "name": "Order confirmation",
"channel": "email", "subject": "Order {{ order_number }} confirmed",
"layout": "transactional", "folder": "transactional",
"body_format": "markdown", "body": "# Thanks..." }],
"purge": false
}'| Field | Type | Description |
|---|---|---|
layouts | array | Layouts to upsert by name: { name, body }. |
templates | array | Templates to upsert by trigger; folders are created from each folder path. |
purge | boolean | When true, removes templates in Messy that aren't in your files. Use with care. |
A 2xx response returns counts of layouts, folders and templates created and updated. 401 means a bad key; 422 returns per-template validation errors.
Reference implementation
A complete sync is short. This is the whole thing, minus the validate and purge niceties:
require "net/http"; require "json"; require "yaml"
MESSY_URL = ENV.fetch("MESSY_API_URL") # https://api.messy.sh
API_KEY = ENV.fetch("MESSY_API_KEY")
DIR = "templates"
def parse_templates(dir)
Dir.glob(File.join(dir, "**/*.md")).flat_map do |path|
folder = File.dirname(path).delete_prefix(dir).delete_prefix("/")
File.read(path).split(/^===\s*$/).filter_map do |section|
_blank, frontmatter, body = section.split(/^---\s*$/, 3)
next if body.nil?
YAML.safe_load(frontmatter).merge(
"folder" => (folder.empty? ? nil : folder),
"body" => body.strip
).compact
end
end
end
def parse_layouts(dir)
file = File.join(dir, "layouts.yml")
File.exist?(file) ? (YAML.safe_load(File.read(file)) || []) : []
end
payload = { layouts: parse_layouts(DIR), templates: parse_templates(DIR), purge: false }
uri = URI("#{MESSY_URL}/sync")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(Net::HTTP::Post.new(uri.path,
"Content-Type" => "application/json", "Authorization" => "Bearer #{API_KEY}"
).tap { |r| r.body = payload.to_json })
end
abort "Sync failed (#{res.code}): #{res.body}" unless (200..299).include?(res.code.to_i)
puts res.bodyWire it into a deploy step or a Make/Rake task so a sync runs whenever your templates change. (Messy itself is synced this way from the Lalaaji codebase via a messy:sync task.)
Generate your own
Prefer it in TypeScript, Python, or Go? Paste the prompt below into your coding assistant and it will produce a sync CLI for your stack, with validate, dry-run and purge built in.
Write a small CLI that syncs message templates from this repository to Messy
(a messaging platform) through its sync API. Use <YOUR LANGUAGE>.
Purpose: keep all message content (email / SMS / WhatsApp / push) in version
control as files, and push them to Messy with one command.
File format, under a "templates/" directory:
- Each .md file holds one or more templates, separated by a line containing
only "===".
- Each template is YAML frontmatter between two lines containing only "---",
followed by the body. Frontmatter keys:
trigger (required, unique handle)
name (required)
channel (email | sms | whatsapp | push; default email)
subject (email only)
preview (optional preheader)
body_format (markdown | html; default markdown)
layout (optional layout name to wrap the body)
- A template's "folder" is its file path relative to templates/ (nested dirs ok).
- Optional templates/layouts.yml: a list of { name, body }, where body contains
a "{{ content }}" slot.
- Optionally replace the token "{{base_url}}" in bodies with an APP_URL env var.
API:
- POST {MESSY_API_URL}/sync (for example https://api.messy.sh/sync)
- Header: Authorization: Bearer {MESSY_API_KEY}
- JSON body:
{ "layouts": [{ "name", "body" }],
"templates": [{ "trigger","name","channel","subject","preview",
"body_format","layout","folder","body" }],
"purge": false }
- 2xx returns created/updated counts. 401 = invalid key. 422 = validation
errors as an array of { trigger, errors }.
Requirements:
- Read MESSY_API_URL and MESSY_API_KEY from the environment.
- Subcommands: "validate" (parse and check required fields locally, no network),
"sync" (push), "sync --purge" (push and remove templates not in the files),
and a global "--dry-run" that prints the payload instead of sending it.
- Exit non-zero with a clear message on 401, 422 or any non-2xx response.
- Keep dependencies minimal: a YAML parser and an HTTP client only.
Output just the script, ready to run.--purge as destructive: it deletes templates in Messy that are missing from your files. Run validate and a --dry-run first.