Customer Emails and Order Invoices
How customer emails are sent and styled, every template and its merge tags, reminders and checkout recovery, and the order invoice and credit note pages and PDFs. Client invoices have their own page: Client Invoices.
How email is sent
Every customer email goes through EmDash’s ctx.email, so you need an email provider plugin installed. Vendwick can’t set the From address, BCC or attachments; ctx.email takes one recipient, optional CC and reply-to, and a text and HTML body.
Every message is written to the mailQueue collection first. That gives you an email log in the admin (Emails page), retries (up to 5 attempts, 15 minutes apart, flushed by the 5-minute cron tick) and a once-only key so the same notice is never sent twice. On a site with no email provider, messages are kept with the status no_provider.
Plain or branded
The Customer emails and reminders module (emails toggle, off by default) decides how emails look:
- Module off: each email is the built-in template rendered as plain text plus a minimal HTML version (heading, paragraphs, one link). Unknown merge tags are left as written.
- Module on: your saved subject and body edits, the branded HTML layout (store name, accent color, logo, footer, support link), order lines when the email is about an order, and the text part. Unknown merge tags are removed. You can turn single templates off; a turned-off template sends nothing.
Branding settings (module on): emails_reply_to, emails_brand_color, emails_logo_media (a media ID) or emails_logo_url (absolute https URL), emails_footer, emails_support_url. The admin can preview a template and send a test.
Reminders and checkout recovery only run when the module is on.
Templates
Tags listed are the ones each default template uses. With the module on, {site_name}, {site_url}, {customer_email} and {recipient} are always available.
| Template | Sent when | Tags in the default |
|---|---|---|
purchase | An order is paid | site_name, order_number, customer_name, next_steps, order_items (0.2.0: the item list, empty for one item of quantity 1), license_keys, dashboard_url |
free_confirm (0.2.0) | A free checkout waits for the buyer’s email confirmation | site_name, product, customer_name, hours, confirm_url |
access | A customer asks for an account link | site_name, dashboard_url |
renewal | A renewal is paid | product, customer_name, expires_at, dashboard_url |
upgraded | An upgrade is paid | product, customer_name, plan, sites, dashboard_url |
admin_order | An order is paid, to admin_order_email (skipped when blank) | site_name, order_number, customer_email, customer_name, order_answers, admin_orders_url |
refunded | An order is fully refunded | order_number, customer_name |
refunded_partial (0.2.0) | A refund that doesn’t complete the order settles | order_number, customer_name, refund_amount, refund_note, refunded_total, order_total, dashboard_url |
admin_dispute (0.2.0) | A dispute opens, to admin_order_email | site_name, order_number, customer_email, dispute_amount, provider, dispute_reason, admin_orders_url |
admin_refund_failed (0.2.0) | The provider fails a refund it had accepted, to admin_order_email | site_name, order_number, customer_email, provider, refund_amount, refund_outcome, admin_orders_url |
order_shipped (0.2.0) | A shipment leaves (Physical Goods and Shipping) | site_name, order_number, customer_name, shipment_items, carrier, tracking_number, tracking_url, shipping_address |
order_delivered (0.2.0) | A shipment is marked delivered (when the delivered email is on in Shipping settings) | site_name, order_number, customer_name, dashboard_url |
reminder_30 | 30 days or less before expiry (more than 7) | product, expires_at, customer_name, renew_url |
reminder_7 | 1 to 7 days before expiry | same |
reminder_0 | Within a day of expiry | same |
expired | 3 to 4 days after expiry | same |
renewal_upcoming | Within 7 days of an automatic renewal | product, expires_at, customer_name, dashboard_url |
payment_failed | A subscription renewal payment fails | product, customer_name, payment_url |
subscription_cancelled | Automatic renewal is turned off | product, customer_name, expires_at, dashboard_url |
checkout_recovery | An unfinished checkout (recovery on) | product, plan, customer_name, coupon_line, checkout_url |
page_access_link | A visitor asks for a page access link | site_name, access_url |
invoice_issued, invoice_reminder, invoice_overdue, invoice_paid, invoice_void, invoice_refunded | Client invoices | see Client Invoices |
The receipt, renewal and upgrade emails also get {invoice_url} and {invoice_pdf_url} (0.2.0) when the order has an invoice; the refund emails (refunded, refunded_partial) get {invoice_pdf_url}. They aren’t in the default text. With the emails module on, the branded layout adds "View your invoice · Download your invoice (PDF)" under any email that has an invoice link, in the HTML and the text part. EmDash email can’t carry attachments, so the PDF is always a link.
Reminders
A daily job looks at licenses that expire within 30 days or expired up to 4 days ago and sends the one stage that’s due:
- Licenses on an automatic provider renewal (active subscription with auto-renew on) only get
renewal_upcoming, within 7 days. - Daily, weekly and one-month terms skip
reminder_30, so a new monthly license doesn’t get a 30-day reminder on day one. - Each stage is sent once per license and expiry date. A renewal moves the expiry date and starts a fresh set.
- Erased customers and invalid addresses are skipped.
Checkout recovery
Off until you turn on recovery_enabled, because it emails people who didn’t buy. Make sure your privacy policy covers it. An hourly job then emails:
- purchase checkouts that are pending, failed or expired, created between 72 hours ago and
recovery_delay_hoursago (1 to 72, default 2); - only while the product and plan are still active;
- once per checkout, at most once a week per person, and never if that email has since paid for the same product.
The link goes back to checkout for the same product and plan. When recovery_coupon is set, it’s added as ?coupon= and described in {coupon_line}. There’s no unsubscribe link.
Order invoices
Turned on by the Invoices module (invoices toggle, off by default).
- Every paid order with a total above zero gets one invoice number. Free orders get none.
- A fully refunded order that has an invoice number gets one credit note number.
- Since 0.2.0 every refund gets its own credit note (see Refunds and Disputes); the one that completes the refund is the order’s own credit note.
- Numbers come from atomic counters, so parallel orders never share one. Order invoices and client invoices share one invoice series.
- A number can be skipped (a gap in the series) when a run dies between allocating and recording it, or when two deliveries of the same order race.
Settings:
| Setting | Default |
|---|---|
invoice_prefix | INV-{year}- ({year} and {month} are filled in when the number is issued) |
credit_note_prefix | CN-{year}- |
invoice_padding | 5 digits (1 to 10) |
invoice_seller_name, invoice_seller_address, invoice_seller_tax_id, invoice_footer | blank (the seller name falls back to the store name) |
The invoice page
GET /_vendwick/invoice?order=<id>&key=<key> shows the printable invoice. Add &doc=credit-note for the credit note, and &refund=<refundId> for one refund’s credit note. Add &format=pdf for any of them as a PDF file.
- Access: the keyed link from emails and the portal, the portal session cookie when the browser sends it, or a signed-in EmDash admin. Any failure shows the same 404 page.
- The page is HTML with a print stylesheet, a "Download PDF" link and a "Print or save as PDF" button.
- It’s sent with
Cache-Control: private, no-store,X-Robots-Tag: noindex, nofollow,Referrer-Policy: no-referrerand a strict Content Security Policy. - When the order paid a client invoice, the page redirects to that client invoice, which carries the same number.
Admins can assign a number or credit note by hand, copy the keyed link and preview the page from the Invoices admin screen. The order screen has an Invoice documents section with the View and PDF links of the invoice and every credit note.
PDF downloads
Added in 0.2.0. Order invoices, credit notes (one per refund) and client invoices download as PDF:
| Document | URL |
|---|---|
| Order invoice | /_vendwick/invoice?order=<id>&key=<key>&format=pdf |
| Credit note | ...&doc=credit-note[&refund=<refundId>]&format=pdf |
| Client invoice | /_vendwick/pay/<code>?key=<key>&format=pdf |
- Same access as the page. The keyed link, the owner’s portal session or an EmDash admin; any failure is the same 404 page as the HTML. A paid client invoice’s order redirects to the client invoice PDF, as the page does.
- Headers:
Content-Type: application/pdf,Content-Disposition: attachment; filename="INV-2026-00042.pdf"(the number, with anything outside letters, digits,.,_and-replaced),Cache-Control: private, no-store,X-Robots-Tag: noindex, nofollow,Referrer-Policy: no-referrer. - Where the links are: "Download PDF" beside "Print" on the invoice page and the client invoice pay page, "Download PDF" and "Credit note … (PDF)" in the portal order view,
{invoice_pdf_url}in emails, the order screen’s Invoice documents section and the client invoice screen’s PDF button. - What it shows: the same document as the page, from the same data, so the numbers always match: seller block (with the Appearance logo and accent color when you saved an Appearance), buyer block, title and number, dates, a status stamp (Paid, Refunded, Partly refunded; for client invoices Awaiting payment, Overdue, Paid, Void, Refunded, Draft), the lines with quantity, unit price and (when the order stored it per line) tax, bundle contents under their bundle, subtotal, discount, shipping, tax with rate, the reverse-charge note, the total, refund notes, the payment reference, your footer and "Page 1 of N". Long invoices continue on more pages with the table header repeated.
Settings
On the Invoices admin screen, under PDF downloads:
| Setting | Default | |
|---|---|---|
invoice_paper_size | a4 | a4 or letter (US Letter) |
invoice_pdf_engine | auto | auto, builtin or browser |
invoice_browser_account_id | blank | Cloudflare account ID (32 hex characters) |
invoice_browser_api_token | blank | Cloudflare API token with the Browser Rendering edit permission (a secret setting, encrypted) |
Preview PDF renders a sample invoice with your seller block, branding, paper size and engine, without storing it.
Engines
- Built-in (always available): a small PDF writer inside Vendwick, no dependencies, works on Workers and Node. It uses the standard PDF fonts (Helvetica, Helvetica-Bold, Courier), which every PDF reader has, so files are small (a one-page invoice is a few kilobytes) but the text is limited to the Windows-1252 (WinAnsi) character set: English and Western European letters, curly quotes, dashes, the euro, pound, yen and cent signs.
- It can’t draw other scripts (Hindi, Arabic, Chinese, Cyrillic, Greek and so on), emoji, or currency symbols outside that set. Letters with marks it lacks lose the mark ("ł" becomes "l", "ő" becomes "o"), currency symbols become their ISO code ("₹" becomes "INR"), and anything else becomes "?". Vendwick’s own amounts are always written with ISO codes ("INR 1,000.00"), so totals are never affected; names, addresses and item titles can be.
- The logo is drawn when the Appearance logo is a media library image in JPEG or PNG (8-bit, not interlaced; transparency is kept). A logo given as a URL, or in another format, is left out of the built-in PDF.
- Cloudflare Browser Rendering (optional): Cloudflare prints the same HTML invoice page in a real browser, so every script and the logo render. It needs a Cloudflare account with Browser Rendering, the account ID and an API token. Vendwick calls
https://api.cloudflare.com/client/v4/accounts/<id>/browser-run/pdf(falling back to the older/browser-rendering/pdfpath) through EmDash’s outbound HTTP, which is why the plugin asks for thenetwork:requestcapability and listsapi.cloudflare.comas an allowed host. Cloudflare bills Browser Rendering by browser time. The request shape follows Cloudflare’s documentation and hasn’t been tested against the live API yet. - Automatic (
auto, the default) uses Browser Rendering only when it’s configured and the document has characters the built-in engine can’t draw.browseruses it for every document.builtinnever calls Cloudflare. Without credentials every setting renders with the built-in engine. - If Browser Rendering fails (an error status, a timeout, something that isn’t a PDF), the built-in engine renders the document instead and the audit log gets an
invoice_pdf_fallbackevent with the reason.
Cache
A generated PDF is stored in private files (the same private storage as release files, never public) at invoices/<kind>/<number>-<hash>.pdf. The hash covers everything on the document plus the paper size, engine and logo, so a changed order, refund, seller setting or Appearance gets a new file and the old one is deleted when the new one is written; a second download of an unchanged document streams the stored file without rendering. Drafts are never stored. When the site has no private files configured, PDFs are rendered on every request. Customer erase deletes the customer’s stored PDFs (see Data, Privacy, Export and Erase).
Limits
- Rendering runs in the request. A one-page invoice takes a few milliseconds of CPU with the built-in engine; very long invoices (hundreds of lines) take longer, and the first download pays that cost once because of the cache. An invoice of 200 or more lines takes about 10 ms of CPU on its first render: fine on Workers Paid, close to the 10 ms per request that the Workers Free plan allows, so a store on the free plan with very long invoices should expect the occasional failed first download (the next one is served from the cache).
- PDFs are rate limited to 30 per minute per visitor IP.