<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TemplateMaster</title>
    <description>The latest articles on DEV Community by TemplateMaster (@templatemaster).</description>
    <link>https://dev.to/templatemaster</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3602366%2F27fd112c-a266-44e5-ab61-f1a6ac1fb48e.png</url>
      <title>DEV Community: TemplateMaster</title>
      <link>https://dev.to/templatemaster</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/templatemaster"/>
    <language>en</language>
    <item>
      <title>Give Your AI Agent the Power to Generate PDFs and Send Emails with an MCP Server</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:49:40 +0000</pubDate>
      <link>https://dev.to/templatemaster/give-your-ai-agent-the-power-to-generate-pdfs-and-send-emails-with-an-mcp-server-385i</link>
      <guid>https://dev.to/templatemaster/give-your-ai-agent-the-power-to-generate-pdfs-and-send-emails-with-an-mcp-server-385i</guid>
      <description>&lt;p&gt;What if your AI assistant could look up an invoice template, fill it with data, generate the PDF and email it to a customer, all from a single prompt?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;TemplateMaster&lt;/a&gt; exposes a &lt;strong&gt;Model Context Protocol (MCP) server&lt;/strong&gt;. Any MCP-compatible agent (a desktop assistant, an IDE agent, or your own) can search your templates, generate documents and send emails, under the same rules as the REST API: permissions, quotas and strict per-organization data isolation.&lt;/p&gt;

&lt;p&gt;In this article we'll connect an agent to it, walk through the permission model, and look at what a typical document-generation run looks like.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 Source: &lt;a href="https://templatemaster.fr/fr/documentation/mcp-server" rel="noopener noreferrer"&gt;official documentation — MCP Server&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔌 Connection details
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://mcp.templatemaster.fr/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Streamable HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X-API-KEY: &amp;lt;API_KEY&amp;gt;&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 &lt;strong&gt;Header gotcha:&lt;/strong&gt; send your API key in the &lt;code&gt;X-API-KEY&lt;/code&gt; header on every request, exactly like the REST API. &lt;code&gt;Authorization: Bearer&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; accepted for API keys; a key sent that way is rejected with a &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ⚙️ Setup in 5 steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create an API key
&lt;/h3&gt;

&lt;p&gt;In TemplateMaster, open &lt;strong&gt;Settings → API Keys&lt;/strong&gt; and click &lt;strong&gt;Generate New Key&lt;/strong&gt;. The same key can serve both the REST API and MCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Enable the key's MCP permissions
&lt;/h3&gt;

&lt;p&gt;On the key's row, click &lt;strong&gt;MCP permissions&lt;/strong&gt; and tick only the capabilities the agent needs. &lt;strong&gt;MCP access is disabled by default&lt;/strong&gt;: a key without any MCP permission is refused by the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure your MCP client
&lt;/h3&gt;

&lt;p&gt;Declare a remote MCP server using the Streamable HTTP transport, the URL above and the &lt;code&gt;X-API-KEY&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templatemaster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.templatemaster.fr/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"X-API-KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_API_KEY"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact config format depends on your MCP client and version, so check its docs for the remote HTTP server syntax. And never commit your API key to a repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Discover the tools
&lt;/h3&gt;

&lt;p&gt;Once connected, your client lists the available tools. &lt;strong&gt;Only the tools allowed by the key's permissions appear.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Make a first request
&lt;/h3&gt;

&lt;p&gt;Ask your agent to list your document templates. It calls &lt;code&gt;list_templates&lt;/code&gt; and displays the result.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Signed requests:&lt;/strong&gt; if &lt;em&gt;Signed requests required&lt;/em&gt; is enabled for your organization (Settings → API Keys), MCP access is refused, because MCP clients can't HMAC-sign every request. Use the standard &lt;em&gt;API Key only&lt;/em&gt; mode to use MCP.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🛡️ Permissions: least privilege by design
&lt;/h2&gt;

&lt;p&gt;Each API key has its own MCP permissions. &lt;strong&gt;Deletions are never available to agents.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Allows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read templates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.templates.read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list_templates&lt;/code&gt;, &lt;code&gt;get_template&lt;/code&gt; and template resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read documents&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.documents.read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_document&lt;/code&gt;, &lt;code&gt;list_documents&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate documents&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.documents.generate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;generate_document&lt;/code&gt; (counts toward the monthly document quota)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read email status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.emails.read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_email_status&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send emails &lt;em&gt;(sensitive)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.emails.send&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;send_email&lt;/code&gt;: &lt;strong&gt;real emails to external recipients&lt;/strong&gt; (counts toward the email quota)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My practical advice: create a &lt;strong&gt;dedicated key per agent&lt;/strong&gt;, and don't grant &lt;code&gt;mcp.emails.send&lt;/code&gt; unless that agent truly needs it. An agent that can only read templates and generate PDFs can't email anything to anyone, whatever a prompt tells it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧰 The tools
&lt;/h2&gt;

&lt;p&gt;Tools always act on the organization that owns the API key. An agent can never reach another organization's data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_templates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search your organization's document and email templates (search, type, pagination)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Template details: type, versions and expected data variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate a PDF from a document template and JSON data. &lt;strong&gt;Asynchronous&lt;/strong&gt;: returns a &lt;code&gt;documentId&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_document&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Document status (&lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;retrying&lt;/code&gt;, &lt;code&gt;generated&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;) and a temporary download link once generated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_documents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List your organization's documents (search, type, period, pagination)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;send_email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send an email from an email template (HTML or MJML) to one or more recipients, with optional attachments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_email_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Processing status of an email (&lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;retrying&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server also exposes read-only resources, &lt;code&gt;templatemaster://templates&lt;/code&gt; and &lt;code&gt;templatemaster://templates/CODE&lt;/code&gt;, with your template catalog.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚶 A typical run
&lt;/h2&gt;

&lt;p&gt;To generate a document, an agent chains these tools. You can do the same by hand in an MCP test client:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list_templates&lt;/code&gt;&lt;/strong&gt;: find the template and copy its code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;get_template&lt;/code&gt;&lt;/strong&gt;: read the variables list to build the &lt;code&gt;data&lt;/code&gt; object (a dot means nesting: &lt;code&gt;customer.name&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;generate_document&lt;/code&gt;&lt;/strong&gt;: send &lt;code&gt;templateCode&lt;/code&gt; and &lt;code&gt;data&lt;/code&gt;, and keep the returned &lt;code&gt;documentId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;get_document&lt;/code&gt;&lt;/strong&gt;: call it until the status is &lt;code&gt;generated&lt;/code&gt;, then open &lt;code&gt;temporaryDownloadUrl&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what the &lt;code&gt;generate_document&lt;/code&gt; arguments look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"templateCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3f2a9c1e-5b7d-4c21-9e0a-7d6b8f1c2a44"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-10023"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T-shirt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19.90"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-10023.pdf"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Everything is asynchronous.&lt;/strong&gt; &lt;code&gt;generate_document&lt;/code&gt; and &lt;code&gt;send_email&lt;/code&gt; answer immediately with an identifier. Poll &lt;code&gt;get_document&lt;/code&gt; / &lt;code&gt;get_email_status&lt;/code&gt; for the outcome.&lt;/li&gt;
&lt;li&gt;🔗 &lt;strong&gt;Download links are short-lived&lt;/strong&gt; (about 60 seconds). Call &lt;code&gt;get_document&lt;/code&gt; again to get a fresh one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💬 What it feels like
&lt;/h3&gt;

&lt;p&gt;Once connected, you can simply say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Generate invoice INV-10023 for John Doe, one T-shirt at 19.90, and give me the PDF."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent finds the &lt;code&gt;invoice&lt;/code&gt; template, reads its variables, builds the JSON above, generates the document, waits for it, and hands you the link. Add the &lt;code&gt;mcp.emails.send&lt;/code&gt; permission and &lt;em&gt;"…then email it to &lt;a href="mailto:john@example.com"&gt;john@example.com&lt;/a&gt;"&lt;/em&gt; works too, which is exactly why that permission is flagged as sensitive.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧯 Error handling
&lt;/h2&gt;

&lt;p&gt;Tool errors come back as a message starting with a stable code, for example &lt;code&gt;TEMPLATE_NOT_FOUND: ...&lt;/code&gt;. No internal detail is leaked; unexpected errors include a correlation ID for support.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNAUTHORIZED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing/invalid/inactive key, key without MCP permission, or key not sent in &lt;code&gt;X-API-KEY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FORBIDDEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The key lacks the permission required by this tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TEMPLATE_NOT_FOUND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unknown, deleted, or belonging to another organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DOCUMENT_NOT_FOUND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unknown document, or belonging to another organization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INVALID_INPUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invalid argument (type, format, size, email address…)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QUOTA_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your plan's monthly limit is reached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GENERATION_FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The document couldn't be generated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EMAIL_FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The email couldn't be queued&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INTERNAL_ERROR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unexpected error: contact support with the correlation ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because the codes are stable, an agent (or your own code) can react to them reliably, for instance by telling the user &lt;em&gt;"quota reached"&lt;/em&gt; instead of retrying forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 Security model
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The organization comes from the API key only.&lt;/strong&gt; No tool accepts an organization ID.&lt;/li&gt;
&lt;li&gt;Another organization's resources are reported &lt;strong&gt;exactly like nonexistent ones&lt;/strong&gt;, so nothing leaks through error messages.&lt;/li&gt;
&lt;li&gt;Inputs are validated and bounded (data size, number of recipients and attachments, pagination), and requests are &lt;strong&gt;rate-limited per key&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Every tool call is &lt;strong&gt;logged and audited&lt;/strong&gt; (MCP source), without logging the data you send.&lt;/li&gt;
&lt;li&gt;Monthly quotas apply exactly as for the REST API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OAuth 2.1 (optional)
&lt;/h3&gt;

&lt;p&gt;On deployments where it's enabled, the MCP server also accepts OAuth 2.1 access tokens issued by your organization's authorization server, in addition to API keys. Tokens go in &lt;code&gt;Authorization: Bearer&lt;/code&gt;; API keys always stay in &lt;code&gt;X-API-KEY&lt;/code&gt;. Contact the TemplateMaster team to enable it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Wrapping up
&lt;/h2&gt;

&lt;p&gt;The MCP server turns document generation into something an agent can do safely: scoped keys, no delete access, strict isolation, quotas, auditing. Start with a read-only key to let the agent explore your templates, then add &lt;code&gt;generate&lt;/code&gt; once you trust the workflow, and keep &lt;code&gt;send&lt;/code&gt; for last.&lt;/p&gt;

&lt;p&gt;Related docs: &lt;a href="https://templatemaster.fr/fr/documentation" rel="noopener noreferrer"&gt;API key configuration&lt;/a&gt; and the .NET integration if you'd rather call TemplateMaster from C# code.&lt;/p&gt;

&lt;p&gt;👉 Full documentation: &lt;a href="https://templatemaster.fr/fr/documentation/mcp-server" rel="noopener noreferrer"&gt;templatemaster.fr/fr/documentation/mcp-server&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Are you already giving agents access to business tools? What guardrails do you put in place? Let me know in the comments! 👇&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>Generate PDFs and Emails in ASP.NET Core with the TemplateMaster NuGet Package</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:47:44 +0000</pubDate>
      <link>https://dev.to/templatemaster/generate-pdfs-and-emails-in-aspnet-core-with-the-templatemaster-nuget-package-20ic</link>
      <guid>https://dev.to/templatemaster/generate-pdfs-and-emails-in-aspnet-core-with-the-templatemaster-nuget-package-20ic</guid>
      <description>&lt;p&gt;If your ASP.NET Core app needs to produce invoices, contracts or transactional emails, you usually end up choosing between a PDF library you template by hand and a separate SaaS you call over HTTP. &lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;TemplateMaster&lt;/a&gt; offers a third option: a NuGet package, &lt;strong&gt;&lt;code&gt;TemplateMaster.AspNetCore&lt;/code&gt;&lt;/strong&gt;, that embeds a visual template editor, its API and a C# service &lt;strong&gt;inside your own application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your users stay your users, your data stays in your database, and generating a PDF becomes a method call instead of an HTTP request.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 Source: &lt;a href="https://templatemaster.fr/fr/documentation/dotnet-integration" rel="noopener noreferrer"&gt;official documentation — .NET Integration (NuGet)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧭 What you get
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Package&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TemplateMaster.AspNetCore&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;.NET 8 · ASP.NET Core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Editor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Visual template editor served under &lt;code&gt;/templatemaster&lt;/code&gt; (UI bundled, no Node.js needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C# API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ITemplateMaster&lt;/code&gt; — generate PDFs and send emails straight from your code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reuses &lt;em&gt;your&lt;/em&gt; app's authentication — there's no separate TemplateMaster login&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  📦 Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.NET 8&lt;/strong&gt; and an ASP.NET Core application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server 2019+&lt;/strong&gt;: a dedicated database for TemplateMaster (created automatically in development)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RabbitMQ in production&lt;/strong&gt; (in development, &lt;code&gt;UseInMemoryMessaging()&lt;/code&gt; is enough)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie-based authentication&lt;/strong&gt; in your app (Identity, OpenID Connect, cookie…). TemplateMaster maps your signed-in users to its own users; it doesn't ship its own sign-in page.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1️⃣ Install the package
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package TemplateMaster.AspNetCore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the only package to reference. The whole TemplateMaster layer is included; only third-party dependencies (EF Core, MassTransit, PuppeteerSharp…) are pulled from nuget.org.&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Configure &lt;code&gt;appsettings.json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;TemplateMaster reads its settings from a dedicated &lt;code&gt;TemplateMaster&lt;/code&gt; section and never touches your other keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"TemplateMaster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=TemplateMaster;Trusted_Connection=True;TrustServerCertificate=True"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RabbitMq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"guest"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TemplateMaster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LicenseKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your license key - keep it in user secrets / environment variables&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PathRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:/templatemaster/files"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"EmailSettings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"SMTPSetting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smtp.example.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"UserName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noreply@example.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PathRoot&lt;/code&gt; is where generated documents are stored on disk (it's printed at startup).&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ Wire it up in &lt;code&gt;Program.cs&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TemplateMaster&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Your application's own authentication (cookie, Identity, OpenID Connect...)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TemplateMasterAdmin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Admin"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTemplateMaster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/templatemaster"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoCreate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TemplateMaster"&lt;/span&gt;&lt;span class="p"&gt;)!)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRabbitMq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMq:Host"&lt;/span&gt;&lt;span class="p"&gt;]!;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMq:Username"&lt;/span&gt;&lt;span class="p"&gt;]!;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMq:Password"&lt;/span&gt;&lt;span class="p"&gt;]!;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLicenseKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TemplateMaster:LicenseKey"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthentication&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// TemplateMaster editor + API, reserved to your administrators&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapTemplateMaster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/templatemaster"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TemplateMasterAdmin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth understanding here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Database.AutoCreate&lt;/code&gt;&lt;/strong&gt; creates the database on first run. Keep it for development only; in production, deploy the schema with the SQL scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dashboard always requires a signed-in user.&lt;/strong&gt; &lt;code&gt;RequireAuthorization(...)&lt;/code&gt; lets you add your own rules on top, like the &lt;code&gt;Admin&lt;/code&gt; role policy above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messaging:&lt;/strong&gt; &lt;code&gt;UseRabbitMq&lt;/code&gt; is for production. For local development, &lt;code&gt;UseInMemoryMessaging()&lt;/code&gt; saves you from running a broker.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4️⃣ Activate your license
&lt;/h2&gt;

&lt;p&gt;Without a license, generated PDFs and emails carry a &lt;strong&gt;watermark&lt;/strong&gt; and the free-plan limits apply: &lt;strong&gt;10 documents and 10 emails per month&lt;/strong&gt;. That's plenty to try everything out.&lt;/p&gt;

&lt;p&gt;The key goes through &lt;code&gt;AddLicenseKey&lt;/code&gt;, matching the &lt;code&gt;TemplateMaster:LicenseKey&lt;/code&gt; entry of your configuration. Since it's a secret, keep it out of source control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Development: user secrets&lt;/span&gt;
dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"TemplateMaster:LicenseKey"&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-key&amp;gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Production: environment variable&lt;/span&gt;
&lt;span class="nv"&gt;TemplateMaster__LicenseKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At startup, the result is written to your logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;info: TemplateMaster.License[0]
      TemplateMaster license: VALID - client acme, issued 2026-10-03, expires 2027-10-03.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the key is rejected, the log explains why, but &lt;strong&gt;an invalid key never prevents your app from starting&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5️⃣ Open the editor
&lt;/h2&gt;

&lt;p&gt;Run the app, sign in with &lt;em&gt;your&lt;/em&gt; application's login, and go to &lt;strong&gt;&lt;code&gt;/templatemaster&lt;/code&gt;&lt;/strong&gt;. A default &lt;code&gt;invoice&lt;/code&gt; template and a default email template are already created for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  👥 Users and organizations
&lt;/h3&gt;

&lt;p&gt;There's no TemplateMaster login screen. A user signed in to your app is automatically matched (by email) to a TemplateMaster user and organization, created on their first visit. Two models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One organization (default):&lt;/strong&gt; all your users share the same templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One organization per customer&lt;/strong&gt; (&lt;code&gt;Tenancy.Mode = HostManaged&lt;/code&gt; + &lt;code&gt;TenantKeySelector&lt;/code&gt;): each customer of your app gets isolated templates and documents, exactly like on the SaaS platform. Perfect for B2B multi-tenant apps.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6️⃣ Generate PDFs and emails from C
&lt;/h2&gt;

&lt;p&gt;Inject &lt;code&gt;ITemplateMaster&lt;/code&gt; into your services. Every call applies the same rules as the REST API: validation, per-organization isolation, plan limits and document history. &lt;strong&gt;No HTTP call involved.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GetTemplatesAsync(type?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the organization's templates (code, name, type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RenderHtmlAsync(code, model)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Render a template to HTML with your data, without saving anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GeneratePdfAsync(code, model, fileName?)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generate a PDF immediately and add it to the document history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QueueDocumentAsync(code, model)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queue a PDF generation, processed in the background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SendEmailAsync(email)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send an email from an email template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ForUser(user)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same API, on behalf of the signed-in user (their organization, their role)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ForTenant(tenantKey)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same API, for a given customer (one-organization-per-customer mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's an invoice service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TemplateMaster&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ITemplateMaster&lt;/span&gt; &lt;span class="n"&gt;templateMaster&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreatePdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoice&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;templateMaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTemplatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TemplateMasterTemplateType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;templateMaster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GeneratePdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;invoice_number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;customer_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And expose it with a minimal API endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/invoices/{id:int}/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InvoiceService&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreatePdfAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;LoadInvoiceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"invoice-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;RequireAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Model property names must match the template variables.&lt;/strong&gt; &lt;code&gt;{{invoice_number}}&lt;/code&gt; expects a property called &lt;code&gt;invoice_number&lt;/code&gt;. Use an anonymous object, a dictionary, or &lt;code&gt;[JsonPropertyName]&lt;/code&gt; to control the names. This is the #1 cause of "empty" fields in generated documents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a PDF takes time or you generate many at once, prefer &lt;code&gt;QueueDocumentAsync&lt;/code&gt;: the work happens in the background through the message bus instead of blocking your request.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚦 Before going to production
&lt;/h2&gt;

&lt;p&gt;A checklist straight from the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Database schema deployed with SQL scripts, &lt;code&gt;Database.AutoCreate&lt;/code&gt; &lt;strong&gt;disabled&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;UseRabbitMq&lt;/code&gt; with a &lt;strong&gt;dedicated virtual host&lt;/strong&gt; if the broker is shared&lt;/li&gt;
&lt;li&gt;[ ] License and secrets (SMTP, &lt;code&gt;Authentication:ClientSecret&lt;/code&gt;) in &lt;strong&gt;environment variables&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;PathRoot&lt;/code&gt; on a &lt;strong&gt;persistent volume&lt;/strong&gt; (shared between instances); &lt;strong&gt;Redis&lt;/strong&gt; configured if you run several instances&lt;/li&gt;
&lt;li&gt;[ ] On Linux / Docker: &lt;strong&gt;Chromium installed&lt;/strong&gt; (&lt;code&gt;/usr/bin/chromium&lt;/code&gt;), required for PDF generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point trips people up in containers: PDF rendering is browser-based (PuppeteerSharp), so a slim base image without Chromium will fail at generation time. A minimal Dockerfile addition on Debian-based images:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; chromium &lt;span class="se"&gt;\
&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 Wrapping up
&lt;/h2&gt;

&lt;p&gt;In a handful of lines you get a visual editor for your non-developer colleagues, a typed C# API for generating PDFs and emails, per-customer isolation if you need it, and no extra login to manage. If you want to see everything working end to end (login, PDF generation, background jobs, emails), the TemplateMaster team published a complete &lt;strong&gt;TemplateMasterDemo&lt;/strong&gt; app on GitHub, linked from the docs.&lt;/p&gt;

&lt;p&gt;Want to try it without any .NET setup first? Check out my previous post on running the whole TemplateMaster stack locally with Docker Compose.&lt;/p&gt;

&lt;p&gt;👉 Full documentation: &lt;a href="https://templatemaster.fr/fr/documentation/dotnet-integration" rel="noopener noreferrer"&gt;templatemaster.fr/fr/documentation/dotnet-integration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Are you generating PDFs in your .NET apps today? Which library are you using? Let me know in the comments! 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aspnet</category>
      <category>csharp</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Run TemplateMaster Locally in 5 Minutes with Docker Compose (PDF &amp; Email API)</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:45:26 +0000</pubDate>
      <link>https://dev.to/templatemaster/run-templatemaster-locally-in-5-minutes-with-docker-compose-pdf-email-api-12kh</link>
      <guid>https://dev.to/templatemaster/run-templatemaster-locally-in-5-minutes-with-docker-compose-pdf-email-api-12kh</guid>
      <description>&lt;p&gt;Generating PDFs and emails from templates is something almost every business application ends up needing: invoices, contracts, notifications, statements… &lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;TemplateMaster&lt;/a&gt; provides a &lt;strong&gt;PDF &amp;amp; Email generation API&lt;/strong&gt; paired with a &lt;strong&gt;visual template editor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Good news: the whole stack can run &lt;strong&gt;locally&lt;/strong&gt; with a single &lt;code&gt;docker compose up&lt;/code&gt;. In this article we'll walk through the official configuration, start it, and I'll share a few tweaks to make it more robust.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 Source: &lt;a href="https://templatemaster.fr/fr/documentation/run-local-environment" rel="noopener noreferrer"&gt;official documentation — Run local environment&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧱 What we're going to run
&lt;/h2&gt;

&lt;p&gt;The local stack is made of 5 services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Port(s)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;editorTemplate-front&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;templatemaster/editortemplate-front&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Web UI / visual editor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8080&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;editorTemplate-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;templatemaster/editortemplate-api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;.NET API (PDF/email generation)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mssql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcr.microsoft.com/mssql/server:2019-latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Database (app + Hangfire)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1433&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rabbitmq-service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rabbitmq:3-management&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message bus for async processing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;5672&lt;/code&gt;, &lt;code&gt;15672&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smtp4dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rnwood/smtp4dev:v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fake SMTP server that captures emails&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;9000&lt;/code&gt;, &lt;code&gt;2525&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            ┌──────────────┐
  Browser   │  Front :8080 │
            └──────┬───────┘
                   │ HTTP
            ┌──────▼───────┐      ┌──────────────┐
            │  API  :5000  ├─────►│ SQL Server   │  (TemplateEditor + HangfireDb)
            └──┬────────┬──┘      └──────────────┘
               │        │
     AMQP      │        │ SMTP
 ┌─────────────▼──┐  ┌──▼──────────┐
 │ RabbitMQ       │  │ smtp4dev    │  → email UI on :9000
 └────────────────┘  └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a classic .NET architecture: &lt;strong&gt;Hangfire&lt;/strong&gt; for scheduled jobs, &lt;strong&gt;RabbitMQ&lt;/strong&gt; for the message queue, and &lt;strong&gt;smtp4dev&lt;/strong&gt; to intercept every email without ever sending anything outside. Perfect for development.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ Before you start: no encryption locally
&lt;/h2&gt;

&lt;p&gt;In production, TemplateMaster encrypts sensitive data in the database. &lt;strong&gt;Locally, this encryption is disabled&lt;/strong&gt; to make debugging easier. Bottom line: never use this environment with real or sensitive data.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt; (Docker Compose is included)&lt;/li&gt;
&lt;li&gt;These ports must be free: &lt;code&gt;5000&lt;/code&gt;, &lt;code&gt;8080&lt;/code&gt;, &lt;code&gt;1433&lt;/code&gt;, &lt;code&gt;5672&lt;/code&gt;, &lt;code&gt;15672&lt;/code&gt;, &lt;code&gt;9000&lt;/code&gt; (and &lt;code&gt;2525&lt;/code&gt; for SMTP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A handy way to check whether a port is already in use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux / macOS&lt;/span&gt;
lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :1433

&lt;span class="c"&gt;# Windows (PowerShell)&lt;/span&gt;
netstat &lt;span class="nt"&gt;-ano&lt;/span&gt; | findstr :1433
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Already running SQL Server on your machine? It's probably holding port 1433. Just change the mapping to &lt;code&gt;"14330:1433"&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🗂️ Project structure
&lt;/h2&gt;

&lt;p&gt;Two files are all you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/
├─ docker-compose.yaml
└─ .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;docker-compose.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;smtp4dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rnwood/smtp4dev:v3&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp4dev&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2525:25"&lt;/span&gt;

  &lt;span class="na"&gt;mssql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/mssql/server:2019-latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mssql&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;SA_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YourStrong!Passw0rd"&lt;/span&gt;
      &lt;span class="na"&gt;ACCEPT_EULA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Y"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1433:1433"&lt;/span&gt;

  &lt;span class="na"&gt;rabbitmq-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rabbitmq:3-management&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rabbitmq_service&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5672:5672"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15672:15672"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RABBITMQ_DEFAULT_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${RABBIT_MQ_USER_NAME}"&lt;/span&gt;
      &lt;span class="na"&gt;RABBITMQ_DEFAULT_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${RABBIT_MQ_USER_PASSWORD}"&lt;/span&gt;

  &lt;span class="na"&gt;editorTemplate-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;templatemaster/editortemplate-api:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;editorTemplate-api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:/EditorTemplate:/app/EditeurDoc"&lt;/span&gt;
      &lt;span class="c1"&gt;#- /home/EditorTemplate:/app/EditeurDoc&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;ConnectionStrings__EditeurBdd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${EDITEUR_BDD}"&lt;/span&gt;
      &lt;span class="na"&gt;ConnectionStrings__HangfireDb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${HANGFIRE_BDD}"&lt;/span&gt;
      &lt;span class="na"&gt;EmailSettings__SMTPSetting__Host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smtp4dev"&lt;/span&gt;
      &lt;span class="na"&gt;EmailSettings__Port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${EDITEUR_EMAIL_PORT}"&lt;/span&gt;
      &lt;span class="na"&gt;EmailSettings__UserName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${EDITEUR_EMAIL_USER_NAME}"&lt;/span&gt;
      &lt;span class="na"&gt;EmailSettings__Password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${EDITEUR_EMAIL_PASSWORD}"&lt;/span&gt;
      &lt;span class="na"&gt;RabbitMQ__HostName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rabbitmq://rabbitmq-service"&lt;/span&gt;
      &lt;span class="na"&gt;RabbitMQ__UserName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${RABBIT_MQ_USER_NAME}"&lt;/span&gt;
      &lt;span class="na"&gt;RabbitMQ__Password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${RABBIT_MQ_USER_PASSWORD}"&lt;/span&gt;
      &lt;span class="na"&gt;contactEmail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CONTACT_EMAIL}"&lt;/span&gt;
      &lt;span class="na"&gt;Authentication__Issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ATHENTICATION_ISSUER}"&lt;/span&gt;
      &lt;span class="na"&gt;Authentication__Audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ATHENTICATION_AUDIENCE}"&lt;/span&gt;
      &lt;span class="na"&gt;Authentication__ClientSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ATHENTICATION_CLIENT_SECRET}"&lt;/span&gt;
      &lt;span class="na"&gt;BaseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${BASE_URL_API}"&lt;/span&gt;
      &lt;span class="na"&gt;BaseUrlFront&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${BASE_URL_FRONT}"&lt;/span&gt;
      &lt;span class="na"&gt;adminEmail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ADMIN_EMAIL}"&lt;/span&gt;
      &lt;span class="na"&gt;adminPassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${ADMIN_PASSWORD}"&lt;/span&gt;
      &lt;span class="na"&gt;LICENSE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${LICENSE_KEY}"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rabbitmq-service&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;smtp4dev&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mssql&lt;/span&gt;

  &lt;span class="na"&gt;editorTemplate-front&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;templatemaster/editortemplate-front:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;editortemplate-front&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${BASE_URL_API}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variables like &lt;code&gt;ConnectionStrings__EditeurBdd&lt;/code&gt; or &lt;code&gt;RabbitMQ__HostName&lt;/code&gt;&lt;/strong&gt; follow the ASP.NET Core convention: the double underscore &lt;code&gt;__&lt;/code&gt; maps to the &lt;code&gt;:&lt;/code&gt; separator in &lt;code&gt;appsettings.json&lt;/code&gt;. So &lt;code&gt;RabbitMQ__HostName&lt;/code&gt; overrides &lt;code&gt;RabbitMQ:HostName&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services talk to each other by name&lt;/strong&gt; on the Docker network: the API reaches the database via &lt;code&gt;Server=mssql&lt;/code&gt;, SMTP via &lt;code&gt;smtp4dev&lt;/code&gt; (internal port &lt;code&gt;25&lt;/code&gt;) and RabbitMQ via &lt;code&gt;rabbitmq-service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The volume&lt;/strong&gt; &lt;code&gt;C:/EditorTemplate:/app/EditeurDoc&lt;/code&gt; persists generated files on the host. On Linux/macOS, uncomment the &lt;code&gt;/home/EditorTemplate:/app/EditeurDoc&lt;/code&gt; line (and comment out the Windows one).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EDITEUR_BDD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Server=mssql;Database=TemplateEditor;Trusted_Connection=False;TrustServerCertificate=True;User id=sa;Password=YourStrong!Passw0rd;'&lt;/span&gt;
&lt;span class="nv"&gt;HANGFIRE_BDD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Server=mssql;Database=HangfireDb;Trusted_Connection=False;TrustServerCertificate=True;User id=sa;Password=YourStrong!Passw0rd;'&lt;/span&gt;

&lt;span class="nv"&gt;EDITEUR_EMAIL_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'smtp4dev'&lt;/span&gt;
&lt;span class="nv"&gt;EDITEUR_EMAIL_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;25
&lt;span class="nv"&gt;EDITEUR_EMAIL_USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'contact@templatemaster.fr'&lt;/span&gt;
&lt;span class="nv"&gt;EDITEUR_EMAIL_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;

&lt;span class="nv"&gt;BASE_URL_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'http://localhost:5000/api/v1'&lt;/span&gt;
&lt;span class="nv"&gt;BASE_URL_FRONT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'http://localhost:8080/'&lt;/span&gt;

&lt;span class="nv"&gt;RABBIT_MQ_USER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'editorTemplate'&lt;/span&gt;
&lt;span class="nv"&gt;RABBIT_MQ_USER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'editorTemplatePassword'&lt;/span&gt;

&lt;span class="nv"&gt;ADMIN_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'admin@templatemaster.fr'&lt;/span&gt;
&lt;span class="nv"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'admin'&lt;/span&gt;
&lt;span class="nv"&gt;CONTACT_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'admin@gmail.com'&lt;/span&gt;

&lt;span class="nv"&gt;ATHENTICATION_ISSUER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'templatemaster.fr'&lt;/span&gt;
&lt;span class="nv"&gt;ATHENTICATION_AUDIENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'account'&lt;/span&gt;
&lt;span class="nv"&gt;ATHENTICATION_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;generate-your-own-secret&amp;gt;'&lt;/span&gt;

&lt;span class="nv"&gt;LICENSE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Watch the spelling&lt;/strong&gt;: the variables really are named &lt;code&gt;ATHENTICATION_*&lt;/code&gt; (no "U"). Don't "fix" them in &lt;code&gt;.env&lt;/code&gt; without also fixing &lt;code&gt;docker-compose.yaml&lt;/code&gt;, or the API will start with empty values.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the authentication secret, rather than copying a value found online, generate your own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🚀 Start it up
&lt;/h2&gt;

&lt;p&gt;Put both files in the same folder, then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;docker compose&lt;/code&gt; (with a space) is the Compose v2 syntax bundled with Docker Desktop. The legacy &lt;code&gt;docker-compose&lt;/code&gt; command works too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the containers are up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="http://localhost:8080/signin" rel="noopener noreferrer"&gt;http://localhost:8080/signin&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;🌐 TemplateMaster UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="http://localhost:9000" rel="noopener noreferrer"&gt;http://localhost:9000&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;📬 smtp4dev — every email sent by the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="http://localhost:15672" rel="noopener noreferrer"&gt;http://localhost:15672&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;📨 RabbitMQ management console (credentials from &lt;code&gt;.env&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://webhook.site" rel="noopener noreferrer"&gt;https://webhook.site&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;🌍 To test your outgoing webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🔑 First login
&lt;/h3&gt;

&lt;p&gt;On first startup, an admin account is created automatically from &lt;code&gt;ADMIN_EMAIL&lt;/code&gt; and &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt;. With the &lt;code&gt;.env&lt;/code&gt; above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email:    admin@templatemaster.fr
Password: admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head over to &lt;a href="http://localhost:8080/signin%E2%80%A6" rel="noopener noreferrer"&gt;http://localhost:8080/signin…&lt;/a&gt; and start building your first template! Send a test email: it will show up instantly in smtp4dev.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Everyday commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Follow the API logs&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; editorTemplate-api

&lt;span class="c"&gt;# Check that .env variables are interpolated correctly&lt;/span&gt;
docker compose config

&lt;span class="c"&gt;# Stop the stack&lt;/span&gt;
docker compose down

&lt;span class="c"&gt;# Update the TemplateMaster images (:latest tag)&lt;/span&gt;
docker compose pull &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 The official docs mention &lt;code&gt;docker compose up -d --build&lt;/code&gt;. Since every service uses a &lt;strong&gt;prebuilt image&lt;/strong&gt; (no &lt;code&gt;build:&lt;/code&gt; section), &lt;code&gt;--build&lt;/code&gt; has no effect here. To get a new version, run &lt;code&gt;docker compose pull&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧠 Going further: making the stack more robust
&lt;/h2&gt;

&lt;p&gt;The official configuration works, but it has a classic pitfall: &lt;strong&gt;&lt;code&gt;depends_on&lt;/code&gt; waits for the SQL Server container to &lt;em&gt;start&lt;/em&gt;, not for it to be *ready&lt;/strong&gt;&lt;em&gt;. SQL Server often takes 10–30 seconds before accepting connections, and the API can crash on startup. The docs actually hint at this: *make sure the database is initialized before the API starts&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fix: add &lt;strong&gt;healthchecks&lt;/strong&gt; and make the API wait for them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mssql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/mssql/server:2019-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MSSQL_SA_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${SA_PASSWORD}"&lt;/span&gt;
      &lt;span class="na"&gt;ACCEPT_EULA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Y"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1433:1433"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mssql-data:/var/opt/mssql&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P "$${MSSQL_SA_PASSWORD}" -Q "SELECT 1"&lt;/span&gt;
        &lt;span class="s"&gt;|| /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$${MSSQL_SA_PASSWORD}" -Q "SELECT 1"&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;

  &lt;span class="na"&gt;rabbitmq-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rabbitmq:3-management&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rabbitmq-diagnostics"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-q"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;editorTemplate-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mssql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;rabbitmq-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;smtp4dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mssql-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ &lt;strong&gt;Healthchecks&lt;/strong&gt; on SQL Server and RabbitMQ → the API only starts once its dependencies actually respond.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Named volume &lt;code&gt;mssql-data&lt;/code&gt;&lt;/strong&gt; → your templates survive a &lt;code&gt;docker compose down&lt;/code&gt; (without &lt;code&gt;-v&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;SA password in &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;SA_PASSWORD=...&lt;/code&gt;) instead of hardcoded in the compose file. Remember to update it in &lt;code&gt;EDITEUR_BDD&lt;/code&gt; and &lt;code&gt;HANGFIRE_BDD&lt;/code&gt; too.&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;MSSQL_SA_PASSWORD&lt;/code&gt; is the variable name Microsoft currently recommends (&lt;code&gt;SA_PASSWORD&lt;/code&gt; is deprecated).&lt;/li&gt;
&lt;li&gt;✅ The &lt;code&gt;version: "3"&lt;/code&gt; line can be removed: Compose v2 ignores it.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The double &lt;code&gt;sqlcmd&lt;/code&gt; in the healthcheck covers both possible tool locations depending on the image version (&lt;code&gt;mssql-tools18&lt;/code&gt; on recent images, &lt;code&gt;mssql-tools&lt;/code&gt; on older ones).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🩺 Quick troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bind for 0.0.0.0:1433 failed: port is already allocated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A local SQL Server is already running&lt;/td&gt;
&lt;td&gt;Change the host port (&lt;code&gt;14330:1433&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The API keeps restarting on first launch&lt;/td&gt;
&lt;td&gt;SQL Server isn't ready yet&lt;/td&gt;
&lt;td&gt;Healthcheck + &lt;code&gt;condition: service_healthy&lt;/code&gt; (see above)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The &lt;code&gt;mssql&lt;/code&gt; container exits immediately&lt;/td&gt;
&lt;td&gt;SA password too weak&lt;/td&gt;
&lt;td&gt;8+ chars with upper/lowercase, digits and symbols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty variables in the API&lt;/td&gt;
&lt;td&gt;Typo / &lt;code&gt;.env&lt;/code&gt; not in the right folder&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker compose config&lt;/code&gt; to see resolved values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No email received&lt;/td&gt;
&lt;td&gt;That's expected! They're captured&lt;/td&gt;
&lt;td&gt;Open &lt;a href="http://localhost:9000" rel="noopener noreferrer"&gt;http://localhost:9000&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume error on Linux/macOS&lt;/td&gt;
&lt;td&gt;Windows &lt;code&gt;C:/...&lt;/code&gt; path&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;/home/EditorTemplate:/app/EditeurDoc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎯 Wrapping up
&lt;/h2&gt;

&lt;p&gt;With two files and one command, you get a complete environment to try TemplateMaster: visual editor, PDF/email generation API, SQL Server database, RabbitMQ bus and email capture. Great for prototyping templates or validating an integration (for example via the .NET NuGet package) before going to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security reminder&lt;/strong&gt;: this environment is for development only — no database encryption, default credentials (&lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;admin&lt;/code&gt;), well-known SA password. Never expose it to the Internet.&lt;/p&gt;

&lt;p&gt;👉 Full documentation: &lt;a href="https://templatemaster.fr/fr/documentation/run-local-environment" rel="noopener noreferrer"&gt;templatemaster.fr/fr/documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you tried it? Tell me in the comments what kind of templates you're generating! 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>docker</category>
    </item>
    <item>
      <title>I Got Tired of Maintaining HTML, PDF and Email Templates in Code, So I Built a Better Solution</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:30:29 +0000</pubDate>
      <link>https://dev.to/templatemaster/i-got-tired-of-maintaining-html-pdf-and-email-templates-in-code-so-i-built-a-better-solution-g30</link>
      <guid>https://dev.to/templatemaster/i-got-tired-of-maintaining-html-pdf-and-email-templates-in-code-so-i-built-a-better-solution-g30</guid>
      <description>&lt;p&gt;If you've ever worked on a business application, you've probably faced this problem.&lt;/p&gt;

&lt;p&gt;You need to generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoices&lt;/li&gt;
&lt;li&gt;quotes&lt;/li&gt;
&lt;li&gt;contracts&lt;/li&gt;
&lt;li&gt;reports&lt;/li&gt;
&lt;li&gt;receipts&lt;/li&gt;
&lt;li&gt;transactional emails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, everything seems simple.&lt;/p&gt;

&lt;p&gt;You create an HTML template, generate a PDF, and move on.&lt;/p&gt;

&lt;p&gt;A few weeks later, the customer asks for small changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can you move the logo?"&lt;/p&gt;

&lt;p&gt;"Can we change the footer?"&lt;/p&gt;

&lt;p&gt;"Can you add a barcode?"&lt;/p&gt;

&lt;p&gt;"Can we have a different template for another customer?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you're editing HTML inside your source code.&lt;/p&gt;

&lt;p&gt;A new deployment is required for every tiny change.&lt;/p&gt;

&lt;p&gt;Eventually, you end up with dozens of template files, duplicated code, and no version history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems I Kept Seeing
&lt;/h2&gt;

&lt;p&gt;After working on several enterprise applications, I noticed the same issues over and over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML templates stored inside the application&lt;/li&gt;
&lt;li&gt;No visual editor&lt;/li&gt;
&lt;li&gt;Every modification required a deployment&lt;/li&gt;
&lt;li&gt;No template versioning&lt;/li&gt;
&lt;li&gt;Difficult collaboration between developers and business teams&lt;/li&gt;
&lt;li&gt;Email templates managed separately from document templates&lt;/li&gt;
&lt;li&gt;No centralized API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It quickly became difficult to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Wanted
&lt;/h2&gt;

&lt;p&gt;I wanted a platform where users could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create templates visually&lt;/li&gt;
&lt;li&gt;manage all templates in one place&lt;/li&gt;
&lt;li&gt;generate PDFs from JSON&lt;/li&gt;
&lt;li&gt;generate HTML emails&lt;/li&gt;
&lt;li&gt;expose everything through a REST API&lt;/li&gt;
&lt;li&gt;keep a complete history of every template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without touching the application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building TemplateMaster
&lt;/h2&gt;

&lt;p&gt;That's why I built &lt;strong&gt;TemplateMaster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple.&lt;/p&gt;

&lt;p&gt;Instead of storing templates inside your application, they're managed in a dedicated platform.&lt;/p&gt;

&lt;p&gt;The application only sends JSON.&lt;/p&gt;

&lt;p&gt;Everything else is handled automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;p&gt;✅ Visual HTML editor&lt;/p&gt;

&lt;p&gt;✅ PDF generation&lt;/p&gt;

&lt;p&gt;✅ Email template generation&lt;/p&gt;

&lt;p&gt;✅ JSON preview&lt;/p&gt;

&lt;p&gt;✅ REST API&lt;/p&gt;

&lt;p&gt;✅ Template versioning&lt;/p&gt;

&lt;p&gt;✅ Document history&lt;/p&gt;

&lt;p&gt;✅ Generated document archive&lt;/p&gt;

&lt;p&gt;✅ Email sending&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning Was a Game Changer
&lt;/h2&gt;

&lt;p&gt;One feature I didn't initially plan became one of my favorites.&lt;/p&gt;

&lt;p&gt;Every template modification automatically creates a new version.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compare versions&lt;/li&gt;
&lt;li&gt;restore previous versions&lt;/li&gt;
&lt;li&gt;see who modified a template&lt;/li&gt;
&lt;li&gt;never lose your work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No more copying files like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invoice_v2_final.html
invoice_v3_final_final.html
invoice_v7_really_final.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've all been there 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  Typical Workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a template visually.&lt;/li&gt;
&lt;li&gt;Connect sample JSON data.&lt;/li&gt;
&lt;li&gt;Preview the generated document.&lt;/li&gt;
&lt;li&gt;Publish the template.&lt;/li&gt;
&lt;li&gt;Call the REST API from your application.&lt;/li&gt;
&lt;li&gt;Generate PDFs or emails on demand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No redeployment required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/v1/document/generate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"templateCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2026-001"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returns the generated PDF.&lt;/p&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building a document generation platform is much more than rendering HTML.&lt;/p&gt;

&lt;p&gt;You also need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;version management&lt;/li&gt;
&lt;li&gt;storage&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;li&gt;preview&lt;/li&gt;
&lt;li&gt;email generation&lt;/li&gt;
&lt;li&gt;PDF rendering&lt;/li&gt;
&lt;li&gt;asset management&lt;/li&gt;
&lt;li&gt;API security&lt;/li&gt;
&lt;li&gt;template history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those parts usually become harder than the rendering itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'm Looking for Feedback
&lt;/h2&gt;

&lt;p&gt;I'm currently improving TemplateMaster and adding new features.&lt;/p&gt;

&lt;p&gt;I'd love to hear how you're currently managing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF generation&lt;/li&gt;
&lt;li&gt;email templates&lt;/li&gt;
&lt;li&gt;document versioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What tools are you using today?&lt;/p&gt;

&lt;p&gt;What do you wish they did better?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fox0venoeix3oqximpla5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fox0venoeix3oqximpla5.png" alt=" " width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Generate Invoices as PDFs via API in C# (Step by Step)</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Wed, 04 Feb 2026 13:02:57 +0000</pubDate>
      <link>https://dev.to/templatemaster/generate-invoices-as-pdfs-via-api-in-c-step-by-step-1c5o</link>
      <guid>https://dev.to/templatemaster/generate-invoices-as-pdfs-via-api-in-c-step-by-step-1c5o</guid>
      <description>&lt;p&gt;Yet in many C# applications, generating a clean and reliable invoice PDF still feels harder than it should be.&lt;/p&gt;

&lt;p&gt;Between PDF libraries, HTML-to-PDF tools, layout bugs, and infrastructure issues, what looks like a simple feature often turns into a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how to generate invoice PDFs in C# using an API-first approach, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML templates&lt;/li&gt;
&lt;li&gt;dynamic JSON data&lt;/li&gt;
&lt;li&gt;no PDF logic in your backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why invoice PDF generation is still painful in .NET&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you’ve worked on invoice generation before, this probably sounds familiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layout breaks when data grows&lt;/li&gt;
&lt;li&gt;PDF libraries tightly coupled to business logic&lt;/li&gt;
&lt;li&gt;Headless browsers crashing in production&lt;/li&gt;
&lt;li&gt;Every layout change requires a redeploy&lt;/li&gt;
&lt;li&gt;Only developers can edit templates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Invoices evolve constantly: branding, legal mentions, taxes, languages.&lt;br&gt;
Embedding all of that directly inside your backend does not scale.&lt;/p&gt;

&lt;p&gt;That’s why treating document generation as a separate service makes a huge difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API-first approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of generating PDFs inside your C# application, we’ll:&lt;br&gt;
(using a document generation API like &lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;TemplateMaster&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an HTML invoice template&lt;/li&gt;
&lt;li&gt;Store and version it in TemplateMaster&lt;/li&gt;
&lt;li&gt;Send invoice data as JSON&lt;/li&gt;
&lt;li&gt;Let the API generate the PDF&lt;/li&gt;
&lt;li&gt;Receive a ready-to-use file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your backend focuses on business logic.&lt;br&gt;
The document layer becomes external, reusable, and maintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 – Create an invoice HTML template&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s a simplified invoice template example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Invoice #{{invoiceNumber}}&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;
  &amp;lt;strong&amp;gt;Customer&amp;lt;/strong&amp;gt;&amp;lt;br /&amp;gt;
  {{customer.name}}&amp;lt;br /&amp;gt;
  {{customer.address}}
&amp;lt;/p&amp;gt;

&amp;lt;table width="100%" border="1" cellspacing="0" cellpadding="8"&amp;gt;
  &amp;lt;thead&amp;gt;
    &amp;lt;tr&amp;gt;
      &amp;lt;th&amp;gt;Description&amp;lt;/th&amp;gt;
      &amp;lt;th&amp;gt;Qty&amp;lt;/th&amp;gt;
      &amp;lt;th&amp;gt;Unit price&amp;lt;/th&amp;gt;
      &amp;lt;th&amp;gt;Total&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
  &amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    {{#each items}}
    &amp;lt;tr&amp;gt;
      &amp;lt;td&amp;gt;{{description}}&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;{{quantity}}&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;{{price}} €&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;{{total}} €&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
    {{/each}}
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;

&amp;lt;p&amp;gt;
  &amp;lt;strong&amp;gt;Total amount:&amp;lt;/strong&amp;gt; {{totalAmount}} €
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is readable&lt;/li&gt;
&lt;li&gt;supports loops&lt;/li&gt;
&lt;li&gt;can be edited by non-developers&lt;/li&gt;
&lt;li&gt;doesn’t require any C# change when updated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2 – Upload the template to TemplateMaster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once uploaded to TemplateMaster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the template is versioned&lt;/li&gt;
&lt;li&gt;editable via a visual editor&lt;/li&gt;
&lt;li&gt;reusable across environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each template has a unique identifier, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;813a5147-a68c-4c37-b194-124100368efa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This identifier will be used by your backend when generating PDFs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 – Prepare invoice data in C#&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now let’s build the invoice payload in C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var invoiceData ={
    "templateCode": "813a5147-a68c-4c37-b194-124100368efa",
    "version": 0,
    "data": {
        "name": "John Doe",
        "address": "1234 Main St, Anytown, AN 12345",
        "items": [
            {
                "description": "Product 1",
                "quantity": 2,
                "price": 30.00
            },
            {
                "description": "Product 2",
                "quantity": 1,
                "price": 15.50
            }
        ],
        "total": 75.50,
        "date": "2023-07-30",
        "metadata": {
            "title": "Service Agreement",
            "author": "TemplateMaster",
            "subject": "Commercial agreement between TemplateMaster and ABC Company",
            "keywords": ["contract", "services", "TemplateMaster", "ABC Company"],
            "creator": "TemplateMaster API",
            "producer": "TemplateMaster PDF Engine",
            "createdAt": "2025-10-14T10:30:00Z",
            "modifiedAt": "2025-10-14T10:31:00Z"
        },
        "indexes":[
            { "DocumentType": "Facture" },
            { "InvoiceNumber":  "F2025-00123" },
            { "CreatedBy": "TemplateMaster" }
        ]
    }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This object matches the variables used in the HTML template.&lt;/p&gt;

&lt;p&gt;No formatting logic.&lt;br&gt;
No PDF concerns.&lt;br&gt;
Just data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 – Call the PDF generation API&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now we send the template ID and data to TemplateMaster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using var client = new HttpClient();

var request = new
{
    templateId = "invoice-default",
    data = invoiceData
};

var response = await client.PostAsJsonAsync(
    "https://api.templatemaster.io/pdf/generate",
    request
);

var pdfBytes = await response.Content.ReadAsByteArrayAsync();
File.WriteAllBytes("invoice.pdf", pdfBytes);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s all it takes.&lt;/p&gt;

&lt;p&gt;No PDF engine.&lt;br&gt;
No headless browser.&lt;br&gt;
No rendering logic in your backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 – Use the PDF anywhere&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once generated, the PDF can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;emailed to customers&lt;/li&gt;
&lt;li&gt;stored for accounting&lt;/li&gt;
&lt;li&gt;archived for compliance&lt;/li&gt;
&lt;li&gt;exposed via an API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the invoice layout changes?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update the template&lt;/li&gt;
&lt;li&gt;No backend redeploy&lt;/li&gt;
&lt;li&gt;No code change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this approach scales better&lt;/strong&gt;&lt;br&gt;
Approach    Problems&lt;br&gt;
PDF libraries   Tight coupling, hard to evolve&lt;br&gt;
HTML-to-PDF in backend  Infra heavy, unstable&lt;br&gt;
API-based generation    Clean, scalable, maintainable&lt;/p&gt;

&lt;p&gt;This pattern works especially well when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoices evolve frequently&lt;/li&gt;
&lt;li&gt;multiple teams collaborate&lt;/li&gt;
&lt;li&gt;you run a SaaS or multi-tenant system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final thoughts&lt;/p&gt;

&lt;p&gt;Invoice PDF generation shouldn’t slow your team down.&lt;/p&gt;

&lt;p&gt;By externalizing document generation behind an API, you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce complexity&lt;/li&gt;
&lt;li&gt;move faster&lt;/li&gt;
&lt;li&gt;keep your backend clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building a serious C# application, this is a pattern worth adopting early.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it locally
&lt;/h3&gt;

&lt;p&gt;If you want to experiment with this approach, I’m using&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;TemplateMaster&lt;/a&gt;&lt;/strong&gt; to manage templates and generate PDFs via API.&lt;/p&gt;

&lt;p&gt;You can try it without changing your C# setup or PDF stack.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>pdf</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Automate Shopify Order Emails &amp; PDF Invoices Using TemplateMaster API + Webhooks</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Tue, 09 Dec 2025 15:16:59 +0000</pubDate>
      <link>https://dev.to/templatemaster/automate-shopify-order-emails-pdf-invoices-using-templatemaster-api-webhooks-4ffb</link>
      <guid>https://dev.to/templatemaster/automate-shopify-order-emails-pdf-invoices-using-templatemaster-api-webhooks-4ffb</guid>
      <description>&lt;p&gt;When a new order is created in Shopify, many merchants want to automatically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate a PDF invoice&lt;br&gt;
Send a confirmation email&lt;br&gt;
Store or archive the document&lt;br&gt;
Trigger backend processes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But doing this manually — or building your own PDF generation engine — is complex and time-consuming.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how to integrate Shopify Webhooks with the TemplateMaster API to automatically send invoices and emails whenever a new order is created.&lt;/p&gt;

&lt;p&gt;This works with any backend language (C#, Node.js, Python, PHP…).&lt;/p&gt;

&lt;p&gt;🧩 What is TemplateMaster?&lt;/p&gt;

&lt;p&gt;TemplateMaster is a SaaS designed for developers and businesses who need:&lt;/p&gt;

&lt;p&gt;📝 HTML/CSS template editor&lt;br&gt;
📄 PDF generation via API&lt;br&gt;
✉️ Email sending API&lt;br&gt;
🔐 API key protection&lt;br&gt;
🧩 JSON-based dynamic rendering&lt;br&gt;
🗂️ Storage + versioning of templates&lt;/p&gt;

&lt;p&gt;It is perfect for invoices, receipts, order confirmations, shipping docs, etc.&lt;/p&gt;

&lt;p&gt;👉 Try it here: &lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;https://templatemaster.fr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📦 Shopify Webhook + TemplateMaster Workflow&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer places an order
⬇️&lt;/li&gt;
&lt;li&gt;Shopify triggers the orders/create webhook
⬇️&lt;/li&gt;
&lt;li&gt;Your server receives the webhook
⬇️&lt;/li&gt;
&lt;li&gt;Your server calls TemplateMaster API to:&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate a PDF invoice&lt;br&gt;
Send an email to the customer (with PDF attached)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;⬇️&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shopify customer receives your customized invoice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;1. Configure the Shopify Webhook&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Shopify Admin:&lt;/p&gt;

&lt;p&gt;Settings → Notifications → Webhooks → Create Webhook&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Event: Order creation (orders/create)&lt;br&gt;
Format: JSON&lt;br&gt;
URL: Your API endpoint (e.g. &lt;a href="https://your-server.com/webhooks/shopify/order-created" rel="noopener noreferrer"&gt;https://your-server.com/webhooks/shopify/order-created&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Shopify will POST the full order JSON to your server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Example of Webhook Receiver (Node.js)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This endpoint receives the Shopify order JSON and generates a PDF invoice through TemplateMaster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import express from "express";
import axios from "axios";

const app = express();
app.use(express.json());

app.post("/webhooks/shopify/order-created", async (req, res) =&amp;gt; {
  const order = req.body;

  try {
    // Call TemplateMaster to generate a PDF + send email
    await axios.post(
      "https://api.templatemaster.fr/generate/pdf-email",
      {
        templateId: "your-invoice-template-id",
        data: order, // You can remap fields if needed
        email: {
          to: order.email,
          subject: `Invoice for Order #${order.name}`
        }
      },
      {
        headers: {
          "X-API-KEY": process.env.TM_API_KEY
        }
      }
    );

    res.status(200).send("OK");
  } catch (err) {
    console.error(err);
    res.status(500).send("Error");
  }
});

app.listen(3000, () =&amp;gt; console.log("Server running"));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Example in C# (.NET)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using System.Net.Http;
using System.Text;

var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-KEY", "YOUR_API_KEY");

app.MapPost("/webhooks/shopify/order-created", async (Order order) =&amp;gt;
{
    var requestBody = new
    {
        templateId = "your-invoice-template-id",
        data = order,
        email = new
        {
            to = order.email,
            subject = $"Invoice for Order {order.name}"
        }
    };

    var json = System.Text.Json.JsonSerializer.Serialize(requestBody);
    var content = new StringContent(json, Encoding.UTF8, "application/json");

    await client.PostAsync("https://api.templatemaster.fr/generate/pdf-email", content);

    return Results.Ok();
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Template Structure (Invoice Example)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside your TemplateMaster HTML template, simply use JSON placeholders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Invoice – Order {{name}}&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;Date: {{created_at}}&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;Customer: {{customer.first_name}} {{customer.last_name}}&amp;lt;/p&amp;gt;

&amp;lt;table&amp;gt;
  &amp;lt;tr&amp;gt;
    &amp;lt;th&amp;gt;Item&amp;lt;/th&amp;gt;
    &amp;lt;th&amp;gt;Qty&amp;lt;/th&amp;gt;
    &amp;lt;th&amp;gt;Price&amp;lt;/th&amp;gt;
  &amp;lt;/tr&amp;gt;

  {{#each line_items}}
  &amp;lt;tr&amp;gt;
    &amp;lt;td&amp;gt;{{title}}&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;{{quantity}}&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;{{price}}&amp;lt;/td&amp;gt;
  &amp;lt;/tr&amp;gt;
  {{/each}}
&amp;lt;/table&amp;gt;

&amp;lt;p&amp;gt;Total: {{total_price}} {{currency}}&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TemplateMaster automatically applies your CSS and returns a beautiful PDF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Bonus Features You Can Add&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;✔️ Send emails to the store owner&lt;br&gt;
✔️ Send packing slips to warehouse staff&lt;br&gt;
✔️ Add customer notes, discount codes, fulfillment info&lt;br&gt;
✔️ Save PDFs to your storage (S3, Azure, etc.)&lt;br&gt;
✔️ Use multiple templates depending on order type&lt;/p&gt;

&lt;p&gt;🎉 Conclusion&lt;/p&gt;

&lt;p&gt;With just a few lines of code, you can fully automate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PDF invoice generation&lt;br&gt;
Order confirmation emails&lt;br&gt;
Document storage&lt;br&gt;
Multi-store workflows&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Using Shopify Webhooks + TemplateMaster API is a powerful way to build scalable, automated document flows without maintaining your own rendering engine.&lt;/p&gt;

&lt;p&gt;👉 Try it now: &lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;https://templatemaster.fr&lt;/a&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>webhooks</category>
      <category>ecommerce</category>
      <category>saas</category>
    </item>
    <item>
      <title>How to Generate PDFs via API in C# (using TemplateMaster)</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Tue, 09 Dec 2025 15:07:39 +0000</pubDate>
      <link>https://dev.to/templatemaster/how-to-generate-pdfs-via-api-in-c-using-templatemaster-47gm</link>
      <guid>https://dev.to/templatemaster/how-to-generate-pdfs-via-api-in-c-using-templatemaster-47gm</guid>
      <description>&lt;p&gt;✨ Introduction&lt;/p&gt;

&lt;p&gt;Generating PDFs dynamically in a C# application is a common need: invoices, quotes, certificates, contracts…&lt;br&gt;
But implementing your own PDF engine is painful — libraries are heavy, HTML rendering is inconsistent, and templates are hard to maintain.&lt;/p&gt;

&lt;p&gt;That's why I built TemplateMaster, a simple API-based service that lets you generate PDF documents from templates using clean JSON data.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how to generate a PDF from C# in just a few lines of code.&lt;/p&gt;

&lt;p&gt;🔧 Step 1 — Add your configuration in appsettings.json&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "TemplateMaster": {
    "ApiKey": "YOUR_API_KEY_HERE",
    "BaseUrl": "https://api.templatemaster.fr"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔨 Step 2 — Create a TemplateMaster API client&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class TemplateMasterClient
{
    private readonly HttpClient _http;
    private readonly string _apiKey;

    public TemplateMasterClient(HttpClient http, IConfiguration config)
    {
        _http = http;
        _apiKey = config["TemplateMaster:ApiKey"];

        _http.BaseAddress = new Uri(config["TemplateMaster:BaseUrl"]);
        _http.DefaultRequestHeaders.Add("X-API-KEY", _apiKey);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This client automatically injects the API key into every request.&lt;/p&gt;

&lt;p&gt;📄 Step 3 — Generate a PDF&lt;/p&gt;

&lt;p&gt;Endpoint used:&lt;br&gt;
&lt;code&gt;POST /documents/generate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;C# method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public async Task&amp;lt;byte[]&amp;gt; GenerateDocument(string templateId, object model)
{
    var request = new
    {
        TemplateId = templateId,
        Data = model
    };

    var response = await _http.PostAsJsonAsync("/documents/generate", request);
    response.EnsureSuccessStatusCode();

    var result = await response.Content.ReadFromJsonAsync&amp;lt;Dictionary&amp;lt;string, string&amp;gt;&amp;gt;();
    var fileUrl = result["FileUrl"];

    return await _http.GetByteArrayAsync(fileUrl);
}

▶️ Usage example
var pdf = await _templateMaster.GenerateDocument("invoice-template", new {
    Customer = "John Doe",
    Amount = 120.50,
    Date = DateTime.Now.ToString("yyyy-MM-dd")
});

await File.WriteAllBytesAsync("invoice.pdf", pdf);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🚀 Try TemplateMaster&lt;/p&gt;

&lt;p&gt;You can test the API for free here:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;TemplateMaster&lt;/a&gt;&lt;/strong&gt; &lt;/p&gt;

</description>
      <category>csharp</category>
      <category>pdf</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Automate Document &amp; Email Generation in Your Apps with TemplateMaster</title>
      <dc:creator>TemplateMaster</dc:creator>
      <pubDate>Sat, 08 Nov 2025 09:48:58 +0000</pubDate>
      <link>https://dev.to/templatemaster/automate-document-email-generation-in-your-apps-with-templatemaster-1p95</link>
      <guid>https://dev.to/templatemaster/automate-document-email-generation-in-your-apps-with-templatemaster-1p95</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2j5wyxgdx0b4arph7s3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm2j5wyxgdx0b4arph7s3.gif" alt=" " width="600" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
Creating, sending, and managing documents and emails is often repetitive and error-prone, especially when working with multiple templates and formats. TemplateMaster is a SaaS platform that allows developers and teams to automate document and email generation using a simple API and an intuitive editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why TemplateMaster?&lt;/strong&gt;&lt;br&gt;
With TemplateMaster, you can:&lt;/p&gt;

&lt;p&gt;Create dynamic templates with HTML/CSS and a live PDF preview.&lt;/p&gt;

&lt;p&gt;Automate email sending directly from your templates or via APIs.&lt;/p&gt;

&lt;p&gt;Store documents securely, or integrate with external storage like AWS S3 or Cloudflare R2.&lt;/p&gt;

&lt;p&gt;Add metadata and document indexes for advanced search or document management (GED).&lt;br&gt;
Advanced Features&lt;/p&gt;

&lt;p&gt;SMTP Configuration: Connect multiple SMTP servers and define a default one.&lt;/p&gt;

&lt;p&gt;Webhooks: Automatically send generated documents to external applications or storage.&lt;/p&gt;

&lt;p&gt;Metadata &amp;amp; Indexing: Add PDF metadata and structured indexes for enterprise document management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
TemplateMaster simplifies the repetitive parts of document and email handling for developers. By integrating the API, you can focus on your core application logic while ensuring reliable, professional documents and emails.&lt;/p&gt;

&lt;p&gt;Try it today: &lt;a href="https://templatemaster.fr" rel="noopener noreferrer"&gt;https://templatemaster.fr&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Repository: [&lt;a href="https://github.com/Moukouk/TemplateMaster" rel="noopener noreferrer"&gt;https://github.com/Moukouk/TemplateMaster&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>emailtemplates</category>
      <category>documentautomation</category>
      <category>saas</category>
      <category>visualtemplateeditor</category>
    </item>
  </channel>
</rss>
