qwicmail
qwicmail docs

Templates

Upload re-usable HTML once. Send by template_id with a JSON data object. qwicmail renders Liquid variables on the way out and ships the result with your DKIM signature and tracking pixels attached.

Why templates

  • One source of truth. Receipts, login codes, and notifications stop drifting across services.
  • Versioned. Every edit creates a new version. Sending continues from the published version until you publish a new one.
  • No build step. Liquid renders in-process — no sidecar, no extra latency.
  • Plain-text fallback. Optional: provide a Liquid text body alongside the HTML and we send a proper multipart/alternative.

The templating language: Liquid

qwicmail uses Shopify Liquid for variable interpolation. It's the same language Shopify, Jekyll, and most marketing-email vendors use, so reference material is abundant. The flavour we support is the standard tag set — {{ var }}, {% if … %}, {% for … %}, filters (upcase, date, default, escape, round, etc.) — minus include / render, which would otherwise open a file-system surface we don't want at send time.

All variable output is HTML-escaped by default. If you need to inject raw HTML (e.g. a pre-rendered fragment), pipe through | raw.

Example template

A receipt email with a conditional discount line and a line-item loop:

HTML + Liquid
<!doctype html>
<html>
  <body style="font-family: system-ui; max-width: 600px">
    <h1>Receipt #{{ order.id }}</h1>
    <p>Hi {{ customer.name | default: "there" }},</p>
    <p>Thanks for your order. Here's what we processed:</p>

    <table style="width:100%; border-collapse: collapse">
      {% for item in order.items %}
        <tr>
          <td>{{ item.name }}</td>
          <td style="text-align:right">
            {{ item.amount | money: order.currency }}
          </td>
        </tr>
      {% endfor %}
    </table>

    {% if order.discount > 0 %}
      <p>Discount applied: {{ order.discount | money: order.currency }}</p>
    {% endif %}

    <p><strong>Total:</strong> {{ order.total | money: order.currency }}</p>
  </body>
</html>

Custom filters available beyond standard Liquid:

  • | money: "NGN" — formats a numeric amount as currency (NGN, USD, GHS, KES, ZAR).
  • | raw — opts out of HTML escaping for this output (use sparingly).

Upload a template

curl https://api.qwicmail.com/templates \
  -H "Authorization: Bearer $QWICMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":    "order_receipt",
    "subject": "Receipt #{{ order.id }}",
    "html":    "<!doctype html>…",
    "text":    "Receipt {{ order.id }} – total {{ order.total }}"
  }'
const tpl = await qm.templates.create({
  name:    "order_receipt",
  subject: "Receipt #{{ order.id }}",
  html:    receiptHTML,
  text:    "Receipt {{ order.id }} – total {{ order.total }}",
});

console.log(tpl.id);
tpl = qm.templates.create(
    name="order_receipt",
    subject="Receipt #{{ order.id }}",
    html=receipt_html,
    text="Receipt {{ order.id }} – total {{ order.total }}",
)

print(tpl.id)
tpl, err := qm.Templates.Create(ctx, &qwicmail.TemplateCreateRequest{
    Name:    "order_receipt",
    Subject: "Receipt #{{ order.id }}",
    HTML:    receiptHTML,
    Text:    "Receipt {{ order.id }} – total {{ order.total }}",
})
tpl = qm.templates.create(
  name:    "order_receipt",
  subject: "Receipt #{{ order.id }}",
  html:    receipt_html,
  text:    "Receipt {{ order.id }} – total {{ order.total }}",
)
$tpl = $qm->templates->create([
    "name"    => "order_receipt",
    "subject" => "Receipt #{{ order.id }}",
    "html"    => $receiptHtml,
    "text"    => "Receipt {{ order.id }} – total {{ order.total }}",
]);
Template tpl = qm.templates().create(TemplateCreateRequest.builder()
    .name("order_receipt")
    .subject("Receipt #{{ order.id }}")
    .html(receiptHtml)
    .text("Receipt {{ order.id }} – total {{ order.total }}")
    .build());

The response includes a template_id (UUID). Subsequent edits to the same template (via PUT /templates/{id}) bump the version automatically; the latest published version is what gets sent.

Send by template_id

Pass template_id and a data object on the regular send call. html / text are derived from the template; the subject is rendered from the template's subject unless you override it on the request.

curl https://api.qwicmail.com/emails \
  -H "Authorization: Bearer $QWICMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from":        "Receipts <receipts@mail.example.com>",
    "to":          "customer@example.org",
    "template_id": "5d2f3a14-…",
    "data": {
      "customer": { "name": "Ada" },
      "order": {
        "id":       "4821",
        "currency": "NGN",
        "total":    27500,
        "discount": 2500,
        "items": [
          { "name": "Subscription — Pro",  "amount": 25000 },
          { "name": "Add-on — Extra IPs",  "amount": 5000  }
        ]
      }
    }
  }'
await qm.emails.send({
  from:       "Receipts <receipts@mail.example.com>",
  to:         "customer@example.org",
  templateId: "5d2f3a14-...",
  data: {
    customer: { name: "Ada" },
    order: {
      id: "4821", currency: "NGN", total: 27500, discount: 2500,
      items: [
        { name: "Subscription — Pro", amount: 25000 },
        { name: "Add-on — Extra IPs", amount: 5000 },
      ],
    },
  },
});
qm.emails.send(
    from_="Receipts <receipts@mail.example.com>",
    to="customer@example.org",
    template_id="5d2f3a14-...",
    data={
        "customer": {"name": "Ada"},
        "order": {
            "id": "4821", "currency": "NGN", "total": 27500, "discount": 2500,
            "items": [
                {"name": "Subscription — Pro", "amount": 25000},
                {"name": "Add-on — Extra IPs", "amount":  5000},
            ],
        },
    },
)
qm.Emails.Send(ctx, &qwicmail.SendRequest{
    From:       "Receipts <receipts@mail.example.com>",
    To:         []string{"customer@example.org"},
    TemplateID: "5d2f3a14-...",
    Data: map[string]any{
        "customer": map[string]any{"name": "Ada"},
        "order": map[string]any{
            "id": "4821", "currency": "NGN", "total": 27500, "discount": 2500,
            "items": []any{
                map[string]any{"name": "Subscription — Pro", "amount": 25000},
                map[string]any{"name": "Add-on — Extra IPs", "amount":  5000},
            },
        },
    },
})
qm.emails.send(
  from:        "Receipts <receipts@mail.example.com>",
  to:          "customer@example.org",
  template_id: "5d2f3a14-...",
  data: {
    customer: { name: "Ada" },
    order: {
      id: "4821", currency: "NGN", total: 27500, discount: 2500,
      items: [
        { name: "Subscription — Pro", amount: 25000 },
        { name: "Add-on — Extra IPs", amount:  5000 },
      ],
    },
  },
)
$qm->emails->send([
    "from"        => "Receipts <receipts@mail.example.com>",
    "to"          => "customer@example.org",
    "template_id" => "5d2f3a14-...",
    "data" => [
        "customer" => ["name" => "Ada"],
        "order" => [
            "id" => "4821", "currency" => "NGN", "total" => 27500, "discount" => 2500,
            "items" => [
                ["name" => "Subscription — Pro", "amount" => 25000],
                ["name" => "Add-on — Extra IPs", "amount" =>  5000],
            ],
        ],
    ],
]);
var data = Map.of(
    "customer", Map.of("name", "Ada"),
    "order", Map.of(
        "id", "4821", "currency", "NGN", "total", 27500, "discount", 2500,
        "items", List.of(
            Map.of("name", "Subscription — Pro", "amount", 25000),
            Map.of("name", "Add-on — Extra IPs", "amount",  5000)
        )
    )
);

qm.emails().send(SendRequest.builder()
    .from("Receipts <receipts@mail.example.com>")
    .to("customer@example.org")
    .templateId("5d2f3a14-...")
    .data(data)
    .build());

Render errors

If the template references a variable that's missing or a syntactically invalid Liquid fragment slips into a stored template, the send is rejected up-front with a clear error:

JSON
{
  "error":   "template_render_failed",
  "message": "undefined variable: order.discount (line 14)"
}

Templates are previewed in the portal before publishing — paste a JSON data document and the portal renders the template against it, showing the rendered subject, HTML, and text bodies side-by-side. Errors there block publishing.

Limits

  • Template HTML body: 1 MiB max.
  • Liquid render time: 250 ms hard timeout. Designed to catch runaway loops in user-supplied templates.
  • Per-tenant template count: 200 (raise on request).

qwicmail uses templates too

The welcome emails, billing receipts, and dunning notices you receive from qwicmail are authored as templates against the same service. If it's good enough for the receipt you got when paying your invoice, it's good enough for yours.