First Invoice With Rendro

Copy Markdown View Source

Render a deterministic invoice PDF from Elixir data, preview it inline, and download the same bytes.

Setup

rendro_dep =
  if System.get_env("RENDRO_LIVEBOOK_LOCAL") == "1" do
    {:rendro, path: System.get_env("RENDRO_LIVEBOOK_PATH") || File.cwd!()}
  else
    {:rendro, "~> 1.0"}
  end

Mix.install([
  rendro_dep,
  {:jason, "~> 1.4"},
  {:jsv, "~> 0.19"},
  {:yaml_elixir, ">= 2.11.0 and < 3.0.0"},
  {:kino, "~> 0.16"}
])

Invoice Data

This mirrors the realistic invoice fixture Rendro ships at priv/examples/invoice/acme-phoenix-saas/invoice.json: a full invoice anatomy with an issuer, a customer, a due date and terms, a handful of distinct line items, and Decimal-faithful totals. Line-item prices are bare numbers (the recipe's legacy line contract); the summary totals are Decimal and are validated to reconcile against the line items.

invoice = %{
  id: "INV-CMP-2026-001",
  date: ~D[2026-06-11],
  due_date: ~D[2026-07-11],
  terms: "Net 30",
  issuer: %{name: "Rendro Systems", address: "100 Deterministic Way, Portland, OR 97205"},
  customer: %{name: "Acme Phoenix SaaS", address: "42 LiveView Lane, Austin, TX 78701"},
  items: [
    %{name: "Platform subscription - Team plan", qty: 1, price: 3600},
    %{name: "Additional seats", qty: 12, price: 79},
    %{name: "Priority support add-on", qty: 1, price: 480}
  ],
  totals: %{
    subtotal: Decimal.new("5028.00"),
    tax: Decimal.new("402.24"),
    total: Decimal.new("5430.24")
  }
}

Render

doc = Rendro.Recipes.Invoice.document(invoice)
{:ok, pdf} = Rendro.render(doc, deterministic: true)

if binary_part(pdf, 0, 5) != "%PDF-" do
  raise "expected Rendro to render a PDF"
end

sha256 = :crypto.hash(:sha256, pdf) |> Base.encode16(case: :lower)

%{
  bytes: byte_size(pdf),
  sha256: sha256
}

Apply a preset

This exact Invoice/Swiss/#2C6BED/light path uses the same invoice data and keeps font registration explicit. To try another canonical choice, edit the preset, accent, and mode values below. Presets are working starting points, not a design-quality, accessibility, PDF/UA, WCAG, or print guarantee. A dark-mode experiment is screen-oriented. Screen-oriented; not a print, accessibility, PDF/UA, or WCAG claim.

# rendro-theme-snippet:start
preset = :swiss

theme =
  Rendro.Theme.preset(preset, accent: {44, 107, 237}, mode: :light)

document =
  invoice
  |> Rendro.Recipes.Invoice.document(theme: theme)
  |> Rendro.Theme.Presets.register_fonts(preset)
# rendro-theme-snippet:end
themed_document = document
{:ok, themed_pdf} = Rendro.render(themed_document, deterministic: true)

if binary_part(themed_pdf, 0, 5) != "%PDF-" do
  raise "expected Rendro to render a themed PDF"
end

themed_sha256 = :crypto.hash(:sha256, themed_pdf) |> Base.encode16(case: :lower)

%{
  bytes: byte_size(themed_pdf),
  sha256: themed_sha256
}
themed_encoded = Base.encode64(themed_pdf)

Kino.HTML.new("""
<iframe
  title="Rendro themed invoice PDF preview"
  src="data:application/pdf;base64,#{themed_encoded}"
  style="width: 100%; height: 640px; border: 1px solid #d1d5db;">
</iframe>
""")
Kino.Download.new(fn -> themed_pdf end, filename: "rendro-invoice-swiss.pdf")

Preview

encoded = Base.encode64(pdf)

Kino.HTML.new("""
<iframe
  title="Rendro invoice PDF preview"
  src="data:application/pdf;base64,#{encoded}"
  style="width: 100%; height: 640px; border: 1px solid #d1d5db;">
</iframe>
""")

Download

Kino.Download.new(fn -> pdf end, filename: "rendro-invoice.pdf")

Realistic Example Library

Rendro also ships the realistic demonstration fixtures used by the Rendered Gallery under priv/examples/ — one per family (invoice, statement, receipt, certificate, payslip, ticket). They are loaded internally through the Rendro.Examples helper and coerced to each recipe's shape by Rendro.ExamplesData. Both are @moduledoc false internal helpers for the shipped demonstration set. Here is the invoice fixture rendered through that internal path:

raw = Rendro.Examples.load!("invoice/acme-phoenix-saas/invoice.json")
fixture_invoice = Rendro.ExamplesData.transform_invoice(raw)

fixture_doc = Rendro.Recipes.Invoice.document(fixture_invoice)
{:ok, fixture_pdf} = Rendro.render(fixture_doc, deterministic: true)

%{
  bytes: byte_size(fixture_pdf),
  sha256: :crypto.hash(:sha256, fixture_pdf) |> Base.encode16(case: :lower)
}

These fixtures are deterministic demonstrations of each family — not a claim of visual polish, and Rendro makes no accessibility-standard claim about the rendered output. In your own app you build the recipe data map directly (as in the section above) and call the recipe's document/2.

Phoenix Handoff

The controller shape is intentionally schematic and is not executed in this notebook.

defmodule MyAppWeb.InvoiceController do
  use MyAppWeb, :controller

  def show(conn, %{"id" => id}) do
    invoice = Billing.get_invoice!(id)
    doc = Rendro.Recipes.Invoice.document(invoice)
    {:ok, pdf} = Rendro.render(doc, deterministic: true)

    conn
    |> put_resp_content_type("application/pdf")
    |> put_resp_header("content-disposition", "inline; filename=invoice-#{id}.pdf")
    |> send_resp(200, pdf)
  end
end

Next

  • Recipes guide: guides/recipes.md
  • Comparison guide: guides/comparison.md
  • Self-rendered manual: assets/rendro/manual.pdf