First Invoice With Rendro
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
}
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