Powered by AppSignal & Oban Pro

First Invoice With Rendro

guides/livebook/first_invoice.livemd

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