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).

repo layout
templates/
  layouts.yml                 # optional shared email wrappers
  transactional/
    order-confirmed.md
    invoice.md
  lifecycle/
    welcome.md                # a file may hold several templates

A template file is YAML frontmatter between two --- lines, followed by the body. Separate multiple templates in one file with a line containing only ===.

templates/transactional/order-confirmed.md
---
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:

templates/layouts.yml
- 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

FieldTypeDescription
triggerreqstringUnique handle used by POST /messages/trigger.
namereqstringHuman-readable name shown in the app.
channelstringemail, sms, whatsapp or push. Defaults to email.
subjectstringEmail subject (Liquid allowed).
previewstringPreheader / preview text.
body_formatstringmarkdown or html. Defaults to markdown.
layoutstringName of a layout (from layouts.yml) to wrap the body.
The token {{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.

POST /sync
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
  }'
FieldTypeDescription
layoutsarrayLayouts to upsert by name: { name, body }.
templatesarrayTemplates to upsert by trigger; folders are created from each folder path.
purgebooleanWhen 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:

bin/messy-sync.rb
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.body

Wire 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.

prompt for your coding LLM
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.
Treat --purge as destructive: it deletes templates in Messy that are missing from your files. Run validate and a --dry-run first.