Build custom Stripe PDF Invoices with Shadcn, Hono and cloudflare workers

Printerz team,

In this article, we will build a basic system that allow to generate custom PDF invoice using Stripe and Cloudflare Workers.

Prerequisites

Create your invoice template

Let's start by creating our invoice template, here we will be using React and Vitejs but you can use any framework and even plain HTML (see details documentation here).

  1. Clone our basic example template
npx degit https://github.com/printerz-app/stripe-custom-invoice-tutorial
  1. Install dependencies
npm install
  1. Start the development server
npm run dev

You can explore the project, remix it and customize it to your needs. Or you can build your own template (check documentation here).

Build your project

Run the following command in order to build your project

npm run build

Upload our template to Printerz 🚀

Our last step is to create a zip file containing file inside your dist folder.

And now we can upload our zip file to printerz !

Create our cloudflare worker

Let's start by scaffold a new Hono/Cloudflare worker project

npx create-hono@latest

Register a R2 binding in your wrangler.toml file

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"

Add secret environment variables in a .dev.vars file

STRIPE_SECRET_KEY=sk_xxxxxx # Gab yours here https://dashboard.stripe.com/apikeys
PRINTERZ_API_KEY=sk_xxxxxx  # Grab yours here https://printerz.dev/dashboard/api-keys

Install stripe sdk

npm install stripe

Create a invoice list route

app.get('/invoices', async (c) => {
  const stripeClient = new Stripe(c.env.STRIPE_SECRET_KEY);
 
  const invoices = await stripeClient.invoices.list({
    limit: 10,
  });
 
  return c.html(
    <html style={{ fontFamily: "sans-serif" }}>
      <body>
        <h1>Invoices</h1>
        <div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
          {invoices.data.map((invoice) => (
            <div style={{ border: "1px solid black", padding: "5px", borderRadius: "5px" }}>
              <h2>{invoice.number}</h2>
              <p>Due: {invoice.due_date ? new Date(invoice.due_date * 1000).toLocaleDateString("en-US") : "N/A"}</p>
              <p>Total: {invoice.amount_paid / 100}</p>
              <a target="_blank" href={`/invoices/${invoice.id}/pdf`}>Download PDF</a>
            </div>
          ))}
        </div>
      </body>
    </html>
  )
})

Create a invoice pdf route

app.get("/invoices/:invoiceId/pdf", async (c) => {
  const stripeClient = new Stripe(c.env.STRIPE_SECRET_KEY);
 
  const invoice = await stripeClient.invoices.retrieve(c.req.param("invoiceId"));
 
  const storageKey = `invoices/${invoice.id}.pdf`;
 
  const storedPDF = await c.env.BUCKET.get(storageKey);
  if (storedPDF) {
    return new Response(storedPDF.body, {
      headers: {
        "Content-Type": "application/pdf",
      }
    })
  }
 
  const renderedPDF = await fetch("https://api.printerz.dev/templates/02c1e66a-6d39-4a1d-b934-8ae646b9419c/render", {
    method: "POST",
    headers: {
      "x-api-key": c.env.PRINTERZ_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      variables: {
        invoiceNumber: invoice.number,
        company: {
          name: invoice.account_name,
          address: "123 Main St, Anytown, USA",
          email: "[email protected]",
          phone: "555-555-5555",
        },
        client: {
          name: invoice.customer_name,
          address: invoice.customer_address,
          email: invoice.customer_email,
        },
        date: new Date(invoice.created * 1000).toLocaleDateString("en-US"),
        dueDate: invoice.due_date ? new Date(invoice.due_date * 1000).toLocaleDateString("en-US") : "N/A",
        items: invoice.lines.data.map((line) => ({
          description: line.description,
          quantity: line.quantity,
          price: line.amount / 100,
          total: (line.amount / 100) * (line.quantity ?? 1),
        })),
        subtotal: invoice.subtotal / 100,
        tax: invoice.tax ? invoice.tax / 100 : 0,
        total: invoice.amount_paid
      },
    })
  });
 
  const pdfBuffer = await renderedPDF.arrayBuffer();
 
  await c.env.BUCKET.put(storageKey, pdfBuffer);
 
  return new Response(pdfBuffer, {
    headers: {
      "Content-Type": "application/pdf",
    }
  })
});

Results

Conclusion

In this guide, we’ve walked through the process of creating custom Stripe PDF invoices using Shadcn, Hono, and Cloudflare Workers. By leveraging Printerz for template rendering and Cloudflare for deployment, you can streamline the generation of dynamic PDFs tailored to your business needs. This setup not only simplifies your workflow but also ensures scalability and efficiency in managing invoices. With these tools, you’re equipped to handle PDF generation effortlessly, allowing you to focus more on growing your application and providing value to your users.

MIT 2025 © Printerz.