Track delivery, opens and clicks on every transactional email a Vendure
server sends. Wraps @vendure/email-plugin, persists every send + open
Maintained by Wayne Garrison.
7-day free trial then £9.95/month subscription, or £199 one-off lifetime at elite.charity/licence/buy/vendure-plugin-email-tracking.
Then add the UI extensions to your compileUiExtensions config:
Every email produced by @vendure/email-plugin (order confirmation,
password reset, OTP, invoice, etc.) is logged automatically — no
per-handler wiring. Inject EmailTrackingService for ad-hoc sends from
your own plugin code and get the same engagement tracking.
/email-track/open/:token.gif — when signingSecret is
configured, the token is <id>.<hmac> and forged ids are rejected./email-track/click/:token?u=… — skips
mailto:, tel:, #-anchors, and unsubscribe/opt-out links.
Optional clickRedirectAllowedDomains denies open-redirector abuse.EmailSuppression entity. Hard bounces and complaints
auto-populate; sendTracked() refuses suppressed recipients and
writes status='suppressed' rather than calling SMTP.GET/POST/DELETE /email-track/suppression.GET /email-track/log/stats/by-template?fromDays=30 returns open
rate, click rate and click-to-open per email type.POST /email-track/bounce with { messageId, status, reason }.X-Signature (HMAC-SHA256) against bounceWebhookSecret or
signingSecret. Postmaster integrations stay loose-coupled.GET /email-track/log/export.csv?status=sent&from=2026-05-01 — same
filters as the list endpoint, up to 50 000 rows.hashIpsInHistory: true (default) stores SHA-256 hashed IPs.Sweeper runs once a day, drops the .unref() flag so it never blocks
shutdown, and silently fails over on DB hiccups.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /email-track/open/:token.gif | public | tracking pixel |
GET | /email-track/click/:token?u=… | public | click redirector |
POST | /email-track/bounce | HMAC | bounce / complaint webhook |
GET | /email-track/log | admin | paginated list with filters |
GET | /email-track/log/summary | admin | status totals tile |
GET | /email-track/log/:id | admin | full detail incl. opens + clicks |
GET | /email-track/log/stats/by-template | admin | per-template aggregates |
GET | /email-track/log/export.csv | admin | CSV export |
GET | /email-track/suppression | admin | list suppressions |
POST | /email-track/suppression | admin | add a suppression |
DELETE | /email-track/suppression/:recipient | admin | lift a suppression |
GET | /email-track/status | admin | version + licence + update status |
Re-send every active key on file at elite.charity/licence/forgot.
User manual + screenshots: huloglobal.com/vendure-plugins/email-tracking/docs/
Commercial. Buy at elite.charity/licence/buy/vendure-plugin-email-tracking.
All notable changes to @huloglobal/vendure-plugin-email-tracking are
documented here. The format follows
Keep a Changelog and this project
adheres to semantic versioning.
dbConnectionOptions use.
Verified against PostgreSQL 17. MySQL/MariaDB installs are unaffected
(byte-identical passthrough).customerId lookup in TrackingEmailSender. If
the subject doesn't contain an order code (password reset, OTP,
email verification, account welcome, etc.) the sender now does a
case-insensitive Customer.emailAddress lookup so the row still
links to the right customer. Combined with 0.8.1's order-code
extraction, this means the per-customer Emails view now surfaces
every send the customer ever received — order-related and
non-order-related.TrackingEmailSender now populates orderCode, orderId and
customerId on every EmailLog row it creates. Before this fix, only
emails sent by our own service code (which passed those ids in
explicitly) had them set — the Vendure email-plugin's built-in
order-confirmation, invoice, password-reset etc. handlers do not
hand order/customer entities through to the sender, so those rows
saved with all three ids as NULL. That broke the per-order and
per-customer Emails buttons on the admin (they filter by
orderCode / customerId), showing an empty list even though the
emails were sent.
The sender now extracts the order code from the email subject via a
#<code> regex (Vendure's default order-related templates render
it there — e.g. "Order confirmation for #S2BZ54TEK91HUUBA"), then
looks up the corresponding Order row to backfill orderId and
customerId. Best-effort: unmatched subjects fall back to the
previous behaviour (envelope-only row).
warnIfIncompatibleVendure(). Logs a non-fatal warning when the runtime
@vendure/core version is outside the tested range. Silent when inside;
fail-open on unparseable versions.@vendure/core tightened to >=3.5.0 <4.0.0 — Vendure 3.5,
3.6 and 3.7 are all covered. Anything under 3.5 has never been tested
by us; anything from 4.0 upwards is deferred until we've seen the
changelog.@huloglobal/vendure-licence-sdk@^0.6.0.EmailLink entity — per-link metadata for tracked transactional
emails. Each clickable link in each email gets its own random 32-byte
token. Records the link's type, human label, visible text, position
index, template section (main_cta, footer, etc.), destination host,
path, and a SHA-256 hash of the full destination URL.EmailLinkService — issues per-link tokens, verifies HMAC
signatures on click, looks up link metadata at redirect time. Graceful
degradation: if the host doesn't have the migration yet, the redirect
still works via signature verification alone; the click event just
won't carry server-side link metadata.isSensitive: true on password-reset,
invoice-access, or licence-delivery URLs. The raw destination never
lands on an admin-visible event row; only the redacted form and hash.@huloglobal/vendure-licence-sdk@^0.5.0 for the shared
classifyEmailEvent() classifier.huloEmailLogs, huloEmailLog, huloEmailStatsByTemplate (paid),
huloEmailSuppressions, huloAddEmailSuppression,
huloRemoveEmailSuppression.npm view --json <pkg> dist.attestations.display: block on mobile tables that broke
the row / cell alignment. Table now scrolls horizontally inside its
card at narrow widths.signingSecret is configured, the URLs
embed an HMAC tag and forged ids are rejected.clickRedirectAllowedDomains).POST /email-track/bounce webhook
(bounceWebhookSecret).hashIpsInHistory, default
true; ipSalt setting)./open + /click.options.retention.@huloglobal/vendure-licence-sdk@^0.2.0.UpdateChecker integration via the licence-sdk — /email-track/status
endpoint returns version + update info, admin UI shows a banner when
a new version is available.EmailSuppression entity. Hard bounces
and complaints auto-add to the table; sendTracked() refuses
recipients on the list and writes status='suppressed'. CRUD
endpoints (GET /email-track/suppression,
POST /email-track/suppression,
DELETE /email-track/suppression/:recipient).GET /email-track/log/stats/by-template
returns open rate, click rate and click-to-open per email type.GET /email-track/log/export.csv mirrors the list
endpoint's filters (max 50 000 rows).opensJson) alongside the existing click
history. Capped to the last 50 opens per email — older opens still
contribute to openCount. Surfaced as opens: [] on
GET /email-track/log/:id.EmailTrackingPlugin — wraps @vendure/email-plugin and persists every
send to the email_log table.TrackingEmailSender — drop-in EmailSender replacement that wraps
the default Nodemailer sender and injects an open-tracking pixel and a
click redirector into the outgoing HTML.EmailTrackingService — exposed for custom controllers that send
transactional email outside the email-plugin pipeline./email-track/open/:id.gif (1×1 pixel),
/email-track/click/:id?u=<encoded> (302 redirect), and
/email-track/bounce (webhook hook for DSN parsers)./email-track/log (paginated list with filters),
/email-track/log/summary and /email-track/log/:id.@huloglobal/vendure-licence-sdk with
revocation polling against the HULO licence server.