Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The reliable pattern is a data-to-template-to-PDF pipeline: load your lead records, render one HTML letter per record with its values, and call wkhtmltopdf for each rendered file. Use deterministic filenames, validate a sample, record failures, and make retries independent of successful letters. For larger batches, --read-args-from-stdin can reduce repeated process-start overhead, but there is no published benchmark proving a particular throughput for 100 letters.

What you are building

wkhtmltopdf converts HTML pages into PDF files with a headless Qt WebKit renderer. The documented command-line manual is for wkhtmltopdf 0.12.6 (with patched qt); that label describes the manual’s version context, not a guarantee that it is the current or best-supported binary for your operating system. Verify the binary source and compatibility before production use.

The converter does not import a CRM or perform a mail merge by itself. Your application supplies that layer: it reads each lead, substitutes values into an HTML template, writes temporary HTML, and invokes the converter. Framework integrations follow the same model: Django’s PDF template view renders an HTML template and can expose header and footer templates, while a Flask wrapper passes Jinja2 values as template context.

Choose the input and output design

Lead fields

Start with the minimum data your letter needs. A typical record has a stable identifier, recipient name, organization, postal address, salutation, and any account-specific text. Keep the identifier separate from display names so output filenames remain safe and repeatable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Topeen 50 Pack Stationery Set with Envelopes, Cream Color, 8.5x11 Inches
  • Cream Color Stationery Set: Includes 50 business envelopes, 50 paper sheets, and 50 gold stickers, all featuring elegant gold border design for professional correspondence
  • Material: Made with 120gsm paper weight for superior writing experience and durability, perfect for business or personal letters
  • #10 Envelopes Size: Standard #10 business envelopes measure 4.13 x 9.5 inches, ideal for mailing letters and documents
  • Paper Size: Full-size 8.5 x 11 inch letter sheets accommodate lengthy correspondence while maintaining a professional appearance
  • Elegant Design: Gold border detailing on both envelopes and paper sheets adds an elegant touch, complemented by matching gold stickers
  • Use a stable ID such as lead_00481 for filenames and logs.
  • Define what happens when a value is missing: use an empty string, a neutral fallback such as “there,” or reject the record for manual correction.
  • Normalize line breaks in addresses and escape values before inserting them into HTML.
  • Keep source data and generated PDFs in separate directories so a rerun cannot silently overwrite your input.

One PDF per lead or one combined PDF

One PDF per lead is usually the safer choice for email attachments, print queues, and retries. A combined document can be useful for internal review, but a single bad record or page-break error then affects a larger artifact. The examples below create one PDF per record.

Build a reusable HTML letter template

Keep layout in one template and values in the record. The example uses Python and Jinja2-style placeholders, but the same fields can be populated by another templating system.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    @page { size: Letter; margin: 22mm 20mm 20mm; }
    body { font-family: Arial, sans-serif; font-size: 11pt; line-height: 1.45; }
    .letterhead { font-weight: 700; margin-bottom: 28px; }
    .address { white-space: pre-line; margin-bottom: 24px; }
    .date { text-align: right; margin-bottom: 24px; }
    .signature { margin-top: 32px; }
  </style>
</head>
<body>
  <div class="letterhead">Example Company</div>
  <div class="date">{{ date }}</div>
  <div class="address">{{ recipient_name }}
{{ organization }}
{{ address }}</div>
  <p>Dear {{ salutation }},</p>
  <p>{{ opening }}</p>
  <p>{{ main_message }}</p>
  <p>Please contact me if you would like to discuss this further.</p>
  <p class="signature">Sincerely,<br>Alex Morgan<br>Example Company</p>
</body>
</html>

Use an HTML-escaping function when inserting untrusted text. If a lead’s address contains & or angle brackets, escaping prevents malformed markup. If you intentionally allow limited rich text, sanitize it separately rather than disabling escaping globally.

Complete Python batch script

This script reads a JSON array, renders a template, creates one PDF per lead, and writes a failure log. It does not claim a benchmark; rendering time depends on the template, assets, operating system, and binary.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from pathlib import Path
from datetime import date
import json, re, subprocess, sys
from jinja2 import Environment, FileSystemLoader, StrictUndefined

INPUT = Path("leads.json")
TEMPLATE_DIR = Path("templates")
TEMPLATE_NAME = "letter.html"
HTML_DIR = Path("rendered_html")
PDF_DIR = Path("letters_pdf")
FAILURE_LOG = Path("letter_failures.jsonl")
WKHTMLTOPDF = "wkhtmltopdf"

for directory in (HTML_DIR, PDF_DIR):
    directory.mkdir(parents=True, exist_ok=True)

def safe_id(value):
    text = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(value)).strip("._")
    if not text:
        raise ValueError("lead has no usable id")
    return text

def clean(value, fallback=""):
    return "" if value is None else str(value).strip() or fallback

def render_lead(env, lead):
    lead_id = safe_id(lead["id"])
    recipient = clean(lead.get("recipient_name"), "there")
    context = {
        "date": clean(lead.get("date"), date.today().isoformat()),
        "recipient_name": recipient,
        "organization": clean(lead.get("organization")),
        "address": clean(lead.get("address")),
        "salutation": clean(lead.get("salutation"), recipient),
        "opening": clean(lead.get("opening")),
        "main_message": clean(lead.get("main_message")),
    }
    html_path = HTML_DIR / f"{lead_id}.html"
    pdf_path = PDF_DIR / f"{lead_id}.pdf"
    html_path.write_text(env.get_template(TEMPLATE_NAME).render(**context), encoding="utf-8")
    command = [WKHTMLTOPDF, "--page-size", "Letter", "--margin-top", "22mm",
               "--margin-right", "20mm", "--margin-bottom", "20mm",
               "--margin-left", "20mm", str(html_path), str(pdf_path)]
    result = subprocess.run(command, text=True, capture_output=True)
    if result.returncode != 0:
        raise RuntimeError(result.stderr.strip() or f"exit code {result.returncode}")
    return pdf_path

def main():
    leads = json.loads(INPUT.read_text(encoding="utf-8"))
    env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)),
                      undefined=StrictUndefined, autoescape=True)
    failures = 0
    with FAILURE_LOG.open("a", encoding="utf-8") as log:
        for lead in leads:
            try:
                output = render_lead(env, lead)
                print(f"OK {lead.get('id')}: {output}")
            except Exception as exc:
                failures += 1
                entry = {"id": lead.get("id"), "error": str(exc)}
                log.write(json.dumps(entry) + "n")
                print(f"FAILED {lead.get('id')}: {exc}", file=sys.stderr)
    return 1 if failures else 0

if __name__ == "__main__":
    raise SystemExit(main())

Install the Python dependency with pip install jinja2, put the template at templates/letter.html, and provide leads.json as an array. For example:

Rank #2
XXINMOH 72 Pieces Stationery Set with Envelopes (48 Paper Sheets + 24 Envelopes) for Letter Writing, Black Border Design Stationery Paper Sheets and Envelope
  • A Stationery Solution - Experience the convenience of our Stationery Set with Envelopes, offering a comprehensive solution for all your letter writing endeavors.
  • Class Black Border Style - Elevate your letter writing experience with our exquisite Stationery Set. The classic design lends an air of sophistication.
  • Reliable Materials - Immerse yourself in the luxury of our Stationery Writing Kit. Crafted from superior 100gsm stationery paper and 120gsm envelopes, each sheet and envelope exudes quality.
  • Classic Size - Rediscover the pleasure of classic-sized letter paper with our 7.25x10.2-inch sheets. The vintage-inspired design, combined with modern elegance, makes our Stationery Writing Kit a addition to your stationary collection.
  • Applicable to Multiple Scenarios - This Stationery Set with Envelopes is not just a letter writing kit; it's a versatile companion for all occasions. From heartfelt letters to professional communications, the set is designed to meet the diverse needs of your stationary requirements.
[{"id":"lead_00481","recipient_name":"Jordan Lee","organization":"Northwind Parts","address":"14 Market StreetnBoston, MA 02110","salutation":"Jordan","opening":"Thank you for speaking with us.","main_message":"We can help consolidate your monthly orders."}]

Run the script from the project directory. It leaves successful PDFs in letters_pdf and appends record-level errors to letter_failures.jsonl. Because each output is named from the lead ID, you can correct failed data and rerun without changing successful files.

Install and invoke wkhtmltopdf directly

Make sure wkhtmltopdf is on PATH, then verify the executable with wkhtmltopdf --version. The manual documents controls for paper size, orientation, margins, headers, footers, and other output behavior.

wkhtmltopdf --page-size Letter 
  --orientation Portrait 
  --margin-top 22mm --margin-right 20mm 
  --margin-bottom 20mm --margin-left 20mm 
  rendered_html/lead_00481.html letters_pdf/lead_00481.pdf

Letter versus A4

The documented configuration supports Letter and A4, with A4 identified as the default in that manual. Choose based on where the letters will be printed or delivered. Do not mix sizes in one campaign unless that is intentional; line wrapping and page breaks can change.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Headers, footers, and letterhead

Use command-line header and footer options when the same page furniture belongs on every document. A framework integration can instead render separate header and footer templates. For a letterhead that must appear only on page one, CSS and page-specific layout need careful testing; inspect more than the first page before sending.

Reduce startup overhead for a large batch

The manual documents --read-args-from-stdin: each line from standard input is treated as a separate invocation. This can help when repeatedly starting the process becomes a bottleneck, but it is not a published speed guarantee for 100 documents.

Rank #3
48 Pieces Stationery Set with Letter Paper and Envelopes(White)
  • Package Includes: You will receive 24 sheets of high-quality kraft letter paper, 12 pieces white linen texture envelopes and 12 pieces brown wax seal stickers.
  • Size Details: Letter paper size: 21cm*14.3cm/8.3*5.6inch, Envelope size: 16.1cm*11.2cm/6*4inch, Sealing sticker size: diameter about 3.2cm/1.2inch.
  • Reliable Material: This envelope is made of high-quality 250gsm thickened paper, which has clear and linen texture, good texture and durability, while the 120gsm kraft letter paper ensures a smooth writing experience without any hindrance, and comes with a sealing sticker to seal the envelope.
  • How to use: There are a variety of sealing methods. You can use the wax seal stickers we provide to seal it according to your needs, or you can use jute rope to tie the envelope.
  • Suitable for Multiple Scenarios: This stationery set with envelopes is more than just a set of letter writing tools, these exquisite envelopes can be used for gift cards, business invitation cards, wedding invitation cards, etc.
wkhtmltopdf --read-args-from-stdin < jobs.txt

Each line in jobs.txt should contain the arguments for one conversion, for example:

--page-size Letter rendered_html/lead_00481.html letters_pdf/lead_00481.pdf
--page-size Letter rendered_html/lead_00482.html letters_pdf/lead_00482.pdf

Generate this file from your script only after paths have been safely quoted for the target environment. If quoting rules become difficult, ordinary subprocess calls are easier to audit. The standard-input mode also makes per-record error handling less direct, so retain a mapping between each job line and its lead ID.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Throughput, reliability, and operational design

Use a queue when rendering blocks requests

The Flask integration documentation describes PDF rendering as potentially resource intensive and recommends Celery when a synchronous request could take too long. Apply the same principle to a web application: enqueue a batch, process records in a worker, expose progress, and let users download completed files rather than holding an HTTP request open.

Make retries safe

  • Write to a temporary filename, then rename it to the final PDF only after a successful exit code.
  • Skip an existing PDF only when its input version is known to match; otherwise include a template or data hash in a manifest.
  • Log the lead ID, command result, stderr, and timestamp.
  • Retry failed records individually so a bad URL, asset, or field does not rerun the entire batch.
  • Keep a manifest of expected IDs and compare it with generated files before distribution.

External assets and fonts

Remote images, stylesheets, and web fonts can delay or alter rendering. Prefer local, versioned assets where possible, and test with the same network permissions used by the worker. A PDF that opens successfully can still have missing logos or substituted fonts, so inspect representative output.

Quality-control checklist

  • Open at least one short and one long letter.
  • Check names, organization, address line breaks, salutation, and date.
  • Inspect page breaks, margins, logo size, font substitution, and blank pages.
  • Confirm the selected paper size matches recipient geography and printing plans.
  • Verify that every expected lead has exactly one PDF and that every failure is explained in the log.

Troubleshooting common failures

“wkhtmltopdf: command not found”

The executable is not installed or is not on the worker’s PATH. Install a compatible build from a source you trust, use its absolute path in the script, and verify with wkhtmltopdf --version.

Rank #4
Sale
Southworth® 100% Cotton Résumé Paper, 8 1/2" x 11", 24 Lb, 100% Recycled, Ivory, Pack of 100
  • Life Is Noteworthy. Create a resume that is noteworthy and will stand out from the crowd with this Southworth Resume Paper!Make a good first impression with this 100 percent Cotton Resume paper. Each sheet is lignin-free and acid-free to resist yellowing, and can be used in copiers as well as inkjet and laser printers for convenience. This Southworth Resume Paper comes 100 sheets per box to ensure there's enough on hand.
  • Resume paper is ideal for resumes, cover letters, and thank-you notes
  • Paper size: 8.5"W x 11"L
  • Printer compatibility: laser, inkjet, copier
  • Acid- and lignin-free

The PDF is blank or missing content

Check the generated HTML first. A template exception, empty data field, inaccessible remote asset, or JavaScript-dependent page can produce incomplete output. Save the HTML for the failed lead and open it in a browser; then rerun the converter with stderr captured.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Names appear as literal placeholders

The template was not rendered before conversion, or the placeholder syntax does not match the rendering engine. Confirm that the script writes a lead-specific HTML file and that it contains the expected name before invoking wkhtmltopdf.

Text or logos overflow onto another page

Reduce content, adjust margins, constrain image dimensions, or add deliberate page-break rules. Test both Letter and A4 if recipients span regions; the same CSS can wrap differently.

Only some records fail

Inspect the per-record log and the corresponding HTML. Common causes are missing required keys, unsafe filename characters, malformed data, or one asset that is unavailable for a particular letter. Fix the record and retry it independently.

The batch is too slow

There is no applicable published benchmark for 100 letters. Measure your own environment, then consider --read-args-from-stdin, local assets, a worker queue, and avoiding unnecessary page resources. Do not promise a completion time until your template and runtime have been measured.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
48 Pieces Stationery Set with Letter Paper and Envelopes(Red)
  • Package Includes: You will receive 24 sheets of high-quality kraft letter paper, 12 pieces red linen texture envelopes and 12 pieces brown wax seal stickers.
  • Size Details: Letter paper size: 21cm*14.3cm/8.3*5.6inch, Envelope size: 16.1cm*11.2cm/6*4inch, Sealing sticker size: diameter about 3.2cm/1.2inch.
  • Reliable Material: This envelope is made of high-quality 250gsm thickened paper, which has clear and linen texture, good texture and durability, while the 120gsm kraft letter paper ensures a smooth writing experience without any hindrance, and comes with a sealing sticker to seal the envelope.
  • How to use: There are a variety of sealing methods. You can use the wax seal stickers we provide to seal it according to your needs, or you can use jute rope to tie the envelope.
  • Suitable for Multiple Scenarios: This stationery set with envelopes is more than just a set of letter writing tools, these exquisite envelopes can be used for gift cards, business invitation cards, wedding invitation cards, etc.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual requirement is capturing rendered web pages rather than generating correspondence from lead data, ScreenshotNeo provides a one-call website screenshot API and MCP server. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing result. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

For a screenshot, use the documented API examples at ScreenshotNeo documentation:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every plan includes the same feature set, including full-page and element capture, device presets, PDF controls, custom CSS and JavaScript, request blocking, cookies and headers, geolocation, caching, signed links, asynchronous jobs, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Pricing is Free for 1,000 shots per month without a card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing provides two months free. Create a free ScreenshotNeo account to start with 1,000 screenshots a month and no card.

Printing and delivery

Generated PDFs can be emailed, archived, or sent to a print workflow. Letter-size printer paper is relevant only when staff or recipients will print; it is not required for digital delivery. Before sending, protect personal data in temporary HTML files and logs according to your organization’s access controls and retention policy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can wkhtmltopdf read a CSV or CRM export directly?

No. Your application must parse the export, map each record to template fields, render HTML, and then call wkhtmltopdf.

Does –read-args-from-stdin guarantee faster generation?

No. The manual presents it as a way to address process-start overhead. Measure it with your own template, assets, and runtime.

Should I use Letter or A4?

Choose the paper size used by the recipients or printer. The documented tool supports both, and A4 is the manual’s stated default configuration.

Can I safely rerun only failed letters?

Yes, if outputs use stable lead IDs and your script logs failures separately. Correct the data, rerun those IDs, and verify the final manifest.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.