← All posts
·8 min read

Generate Consistent Beautiful Receipts with LangChain and AgentGen

Receipts seem trivial until you need to generate thousands of them consistently. Different line items, amounts, currencies, tax rates — they all need to look identical and professional every time. In this tutorial we'll build a LangChain agent that produces beautiful, brand-consistent PDF receipts using AgentGen, all from a natural-language request or a structured API call.

What we're building

A generate_receipt LangChain tool that accepts order data and returns a CDN download URL for a polished PDF. The receipt includes:

  • A branded header with your company name and accent color
  • Order metadata — order ID, date, payment method
  • Itemized line items with quantity, unit price, and row total
  • Subtotal, tax, and grand total with proper currency formatting
  • Customer info and a thank-you footer

The receipt HTML template

The foundation is a Python dataclass + template function. Using inline styles guarantees Chromium renders it identically every time, regardless of system fonts or environment:

from dataclasses import dataclass, field
from typing import List

@dataclass
class LineItem:
    description: str
    quantity: int
    unit_price: float

@dataclass
class OrderData:
    order_id: str
    date: str
    customer_name: str
    customer_email: str
    line_items: List[LineItem]
    tax_rate: float = 0.08
    currency_symbol: str = "$"
    payment_method: str = "Credit card"
    brand_name: str = "My Store"
    brand_color: str = "#4f46e5"

def build_receipt_html(order: OrderData) -> str:
    subtotal = sum(item.quantity * item.unit_price for item in order.line_items)
    tax = subtotal * order.tax_rate
    total = subtotal + tax
    sym = order.currency_symbol

    rows = "".join(
        f"""<tr>
          <td style="padding:10px 0;color:#374151;border-bottom:1px solid #f3f4f6">{item.description}</td>
          <td style="padding:10px 0;text-align:center;color:#6b7280;border-bottom:1px solid #f3f4f6">{item.quantity}</td>
          <td style="padding:10px 0;text-align:right;color:#374151;border-bottom:1px solid #f3f4f6">{sym}{item.unit_price:.2f}</td>
          <td style="padding:10px 0;text-align:right;font-weight:600;border-bottom:1px solid #f3f4f6">{sym}{item.quantity * item.unit_price:.2f}</td>
        </tr>"""
        for item in order.line_items
    )

    return f"""<!DOCTYPE html>
    <html><head><meta charset="UTF-8"></head>
    <body style="margin:0;padding:0;background:#f9fafb;font-family:'Helvetica Neue',Arial,sans-serif">
      <div style="max-width:600px;margin:40px auto;background:white;border-radius:12px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,0.08)">

        <!-- Brand header -->
        <div style="background:{order.brand_color};padding:28px 36px;color:white">
          <h1 style="margin:0 0 4px;font-size:22px;font-weight:800">{order.brand_name}</h1>
          <p style="margin:0;opacity:0.8;font-size:13px">Order Receipt</p>
        </div>

        <!-- Order meta -->
        <div style="padding:20px 36px;background:#f8faff;border-bottom:1px solid #e5e7eb;display:flex;gap:32px">
          <div>
            <p style="margin:0 0 3px;font-size:10px;text-transform:uppercase;color:#9ca3af;letter-spacing:.06em">Order ID</p>
            <p style="margin:0;font-weight:700;color:#111">#{order.order_id}</p>
          </div>
          <div>
            <p style="margin:0 0 3px;font-size:10px;text-transform:uppercase;color:#9ca3af;letter-spacing:.06em">Date</p>
            <p style="margin:0;font-weight:600;color:#374151">{order.date}</p>
          </div>
          <div>
            <p style="margin:0 0 3px;font-size:10px;text-transform:uppercase;color:#9ca3af;letter-spacing:.06em">Payment</p>
            <p style="margin:0;font-weight:600;color:#374151">{order.payment_method}</p>
          </div>
        </div>

        <!-- Customer -->
        <div style="padding:16px 36px;border-bottom:1px solid #e5e7eb">
          <p style="margin:0 0 2px;font-size:12px;color:#9ca3af">Billed to</p>
          <p style="margin:0;font-weight:600;color:#111">{order.customer_name}</p>
          <p style="margin:0;color:#6b7280;font-size:13px">{order.customer_email}</p>
        </div>

        <!-- Line items -->
        <div style="padding:0 36px">
          <table style="width:100%;border-collapse:collapse;margin:20px 0">
            <thead>
              <tr>
                <th style="text-align:left;font-size:10px;text-transform:uppercase;color:#9ca3af;padding-bottom:8px;border-bottom:2px solid #e5e7eb">Item</th>
                <th style="text-align:center;font-size:10px;text-transform:uppercase;color:#9ca3af;padding-bottom:8px;border-bottom:2px solid #e5e7eb">Qty</th>
                <th style="text-align:right;font-size:10px;text-transform:uppercase;color:#9ca3af;padding-bottom:8px;border-bottom:2px solid #e5e7eb">Price</th>
                <th style="text-align:right;font-size:10px;text-transform:uppercase;color:#9ca3af;padding-bottom:8px;border-bottom:2px solid #e5e7eb">Total</th>
              </tr>
            </thead>
            <tbody>{rows}</tbody>
          </table>
        </div>

        <!-- Totals -->
        <div style="padding:0 36px 28px">
          <div style="margin-left:auto;max-width:240px">
            <div style="display:flex;justify-content:space-between;padding:5px 0;color:#6b7280;font-size:14px">
              <span>Subtotal</span><span>{sym}{subtotal:.2f}</span>
            </div>
            <div style="display:flex;justify-content:space-between;padding:5px 0;color:#6b7280;font-size:14px">
              <span>Tax ({int(order.tax_rate * 100)}%)</span><span>{sym}{tax:.2f}</span>
            </div>
            <div style="display:flex;justify-content:space-between;padding:12px 0 0;font-size:18px;font-weight:800;color:{order.brand_color};border-top:2px solid #e5e7eb;margin-top:6px">
              <span>Total</span><span>{sym}{total:.2f}</span>
            </div>
          </div>
        </div>

        <!-- Footer -->
        <div style="background:#f9fafb;padding:18px 36px;text-align:center;border-top:1px solid #e5e7eb">
          <p style="margin:0 0 3px;font-weight:600;color:#111">Thank you for your order!</p>
          <p style="margin:0;color:#9ca3af;font-size:12px">Questions? Reply to your order confirmation email.</p>
        </div>

      </div>
    </body></html>"""

The LangChain tool

Wrap the template and API call in a StructuredTool with a Pydantic schema the LLM can fill in from conversation context:

import os
import requests
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool
from typing import List, Optional

class LineItemInput(BaseModel):
    description: str
    quantity: int
    unit_price: float

class GenerateReceiptInput(BaseModel):
    order_id: str = Field(description="Unique order identifier, e.g. ORD-2847")
    date: str = Field(description="Human-readable order date, e.g. March 2, 2026")
    customer_name: str
    customer_email: str
    line_items: List[LineItemInput]
    tax_rate: float = Field(default=0.08, description="Tax rate as a decimal, e.g. 0.08 for 8%")
    payment_method: Optional[str] = "Credit card"
    brand_name: Optional[str] = "My Store"
    brand_color: Optional[str] = "#4f46e5"

def _generate_receipt(
    order_id: str, date: str, customer_name: str, customer_email: str,
    line_items: list, tax_rate: float = 0.08,
    payment_method: str = "Credit card",
    brand_name: str = "My Store", brand_color: str = "#4f46e5",
) -> dict:
    order = OrderData(
        order_id=order_id, date=date,
        customer_name=customer_name, customer_email=customer_email,
        line_items=[
            LineItem(**item) if isinstance(item, dict) else item
            for item in line_items
        ],
        tax_rate=tax_rate, payment_method=payment_method,
        brand_name=brand_name, brand_color=brand_color,
    )
    html = build_receipt_html(order)
    response = requests.post(
        "https://www.agent-gen.com/api/v1/generate/pdf",
        headers={"X-API-Key": os.environ["AGENTGEN_API_KEY"], "Content-Type": "application/json"},
        json={"html": html, "format": "A4"},
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()
    return {"receipt_url": data["url"], "tokens_used": data.get("tokens_used", 2)}

generate_receipt_tool = StructuredTool.from_function(
    func=_generate_receipt,
    name="generate_receipt",
    description=(
        "Generate a beautiful branded PDF receipt for an order. "
        "Returns a permanent CDN download URL. Cost: 2 tokens."
    ),
    args_schema=GenerateReceiptInput,
)

Wiring into an agent

Attach the tool to a Claude agent and let it handle the extraction and generation end-to-end:

from langchain_anthropic import ChatAnthropic
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

llm = ChatAnthropic(model="claude-opus-4-6", temperature=0)

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are an order management assistant. When given order details, "
        "use the generate_receipt tool to create a PDF receipt and share the download link. "
        "Confirm the total before generating.",
    ),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(llm, [generate_receipt_tool], prompt)
executor = AgentExecutor(agent=agent, tools=[generate_receipt_tool], verbose=True)

result = executor.invoke({
    "input": (
        "Generate a receipt for order #ORD-2847. "
        "Customer: Sarah Chen ([email protected]). "
        "Items: 2x Wireless Headphones at $89.99, 1x USB-C Cable at $12.99. "
        "Payment: Visa ending 4242. Date: March 2, 2026."
    )
})
print(result["output"])
# → "Here's the receipt for order #ORD-2847: https://cdn.agent-gen.com/output/..."

Batch generation — all of today's orders

For end-of-day batch processing, skip the agent and call the function directly with asyncio to generate hundreds of receipts concurrently:

import asyncio
import aiohttp

async def generate_receipt_async(session: aiohttp.ClientSession, order: OrderData) -> tuple[str, str]:
    html = build_receipt_html(order)
    async with session.post(
        "https://www.agent-gen.com/api/v1/generate/pdf",
        headers={"X-API-Key": os.environ["AGENTGEN_API_KEY"]},
        json={"html": html, "format": "A4"},
    ) as resp:
        data = await resp.json()
        return order.order_id, data["url"]

async def generate_all_receipts(orders: list[OrderData]) -> dict[str, str]:
    async with aiohttp.ClientSession() as session:
        results = await asyncio.gather(
            *(generate_receipt_async(session, o) for o in orders)
        )
    return dict(results)  # {order_id: pdf_url}

# Generate 50 receipts in parallel
receipt_urls = asyncio.run(generate_all_receipts(todays_orders))
# → {"ORD-2847": "https://cdn...", "ORD-2848": "https://cdn...", ...}

Sending the receipt

The PDF URL is a permanent CDN link valid for 30 days — paste it directly into your transactional email, Slack message, or SMS:

import smtplib
from email.mime.text import MIMEText

def email_receipt(to: str, name: str, order_id: str, pdf_url: str):
    msg = MIMEText(
        f"Hi {name},

Your receipt for order #{order_id} is ready:
{pdf_url}

Thank you!",
        "plain",
    )
    msg["Subject"] = f"Your receipt for order #{order_id}"
    msg["From"] = "[email protected]"
    msg["To"] = to

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
        smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
        smtp.send_message(msg)

Cost

Each receipt is one A4 page = 2 tokens. At the Growth tier ($39 for 2,500 tokens) that's 1,250 receipts for $39 — about 3 cents each. Tokens never expire, so unused balance carries over indefinitely.

Get your free API key → and generate your first receipt in minutes.

Ready to start generating?

Create a free account and generate your first PDF or image in minutes.

Get started free