Reference

Changelog

Release notes for every published CampaignCanvas version, newest first.

1.3.2

  • New — tell browsers and AI assistants what each field asks for. Text, email, number, date, long text and dropdown fields have an Autocomplete setting in the form builder. Pick what the field asks for, such as first name, postal code or country, and browsers, password managers and AI assistants that fill in forms for visitors can enter the right value. Email fields use email unless you pick something else. Other fields without a setting work as before.
  • New — phone number fields. Set a text field's Input type to Phone number. Phones then show a number keypad, and browsers can fill in a saved phone number. The answer is stored as text, as before.
  • Improved — errors are linked to their field. When the form's check in the browser finds a problem, the field is now marked as invalid and its error message is linked to it, so screen readers and AI assistants can tell which field failed and why. Help text under a field is linked the same way. Both marks clear as soon as the visitor corrects the field.
  • Improved — screen readers announce the success message. The confirmation shown after a successful submission is now read out by screen readers.
  • Improved — clearer message when a submission looks like spam. A submission caught by the honeypot, or sent within two seconds of the page loading, used to show only "Submission rejected." It now says "We couldn't accept your submission. Please wait a few seconds and try again." Change the text per form under Settings → Validation → Spam rejection message.

1.3.1

  • New — sort the Forms overview by any column. A new Last updated column shows when each form was last saved. Click the Name, Fields, Submissions or Last updated header to sort by it, and click it again to reverse the order. Names sort the way people read them, so Form 2 comes before Form 10. Until you pick a column, the list keeps its usual order.
  • New — pre-select an option on dropdowns and radio buttons. Dropdown and radio fields have a Default value setting in the form builder. Pick one of the field's options and it's already selected when the form loads; visitors can still choose another. A radio group with a default can't be left empty, so leave it on None if visitors must make an active choice. Renaming the option keeps it as the default, and removing it clears the default. Fields without a default work as before.
  • New — switch a field between dropdown, radio buttons and checkbox list. Select one of these fields in the form builder and pick another type under Field type. The field keeps its label, alias and options, so you don't have to set it up again. A checkbox list has no default value, but it remembers the one you set and uses it again when you switch back to a dropdown or radio buttons. Submissions you already received are unchanged. A checkbox list submits a list of choices rather than a single one, so check any webhook or automation that reads the field after switching to or from it.
  • New — show radio buttons and checkboxes in columns. Radio button and checkbox list fields have a Columns setting in the form builder. Pick 2, 3 or 4 columns to lay the options out side by side, so long lists take less space. On narrower forms three or four columns drop to two, and on phones the options stack. Existing fields keep their single column.
  • New — show or hide a field based on any page. On a multi-page form, the field lists under Visible when and Add condition now include the fields from every page instead of only the current one, grouped under each page's name. A field on a later page can depend on an answer given on page 1.
  • New — see which pages use a form. A form's Settings tab starts with an Info section that lists the pages using the form. Click a page to open it in a new tab. Each page shows whether the live page has the form (Published), only the unpublished latest version has it (Draft only), or the live page still has it but the latest version doesn't (Removed in draft). The list covers pages that pick the form in a CampaignCanvas Form Picker, directly or inside blocks; a form added straight from a template isn't listed.
  • Fix — trial. We fixed an issue with the trial setup.
  • Fix — a site could stop saving until it was restarted. The hourly clean-up that deletes expired submissions could collide with other background work and leave a database transaction open. From then on, saves, backoffice sign-ins and background jobs failed — on SQLite with "database is locked" — until the site was restarted. The clean-up now runs isolated from other background work.
  • Fix — visitors could skip a required radio group or checkbox list. The form's check in the browser let these fields through empty. On a multi-page form a visitor could click Next without answering, and only learned what was missing when the server sent the form back with an error after Submit. The browser now checks them like every other required field: at least one option must be chosen before the visitor can continue or submit, and the error appears once, below the options. The server still rejects an empty required field, as before.
  • Fix — conditions based on a checkbox list didn't work reliably. In the browser, a field or group set to appear when a checkbox list equals or contains an option looked only at the first option the visitor ticked. Ticking that option alongside an earlier one kept the field hidden. On submit, the server compared the whole list as one piece of text, so those conditions never matched there. An answer typed into a field they revealed was thrown away instead of stored, and a workflow based on a checkbox list didn't run (or, with not equals, always ran). The browser and the server now agree: a condition matches when any ticked option matches it, not equals matches when that option isn't ticked, and is empty matches when nothing is ticked. Conditions on single checkboxes and radio buttons work as before.
  • Fix — forms in a narrow column didn't rearrange their fields. Field widths are meant to follow the width of the form, not the screen: below 640 pixels, third- and quarter-width fields go two to a row, and at 560 pixels or less every field takes the full width. Those rules never took effect, so fields only stacked once the browser window itself was 560 pixels wide or less, and on a wide screen a form in a sidebar or narrow column kept three or four fields to a row. The layout now follows the form's own width. The form's stylesheet address also includes the package version now, so after an upgrade visitors' browsers load the new styles, such as the new option columns, right away instead of reusing the previous release's copy from their cache.

1.3.0

  • New — promote forms, email templates and newsletter settings from your pipeline. Two token-protected endpoints, GET /api/v1/campaigncanvas/public/package/export and POST /api/v1/campaigncanvas/public/package/import, let a CI job copy your configuration from staging to live without anyone signing in to the backoffice. Set CampaignCanvas:PackageApi:ExportToken on the environment you export from and CampaignCanvas:PackageApi:ImportToken on the one you import into — each token opens only its own direction, so a token leaked from staging can't write anything to live. A token must be at least 32 characters (openssl rand -hex 32 makes a good one), and an endpoint with no usable token stays switched off. After ten wrong attempts from one address within five minutes, that address is refused until the window ends, and every export and import — from CI or from the backoffice — is recorded in the audit log. An import through the API never touches subscribers, lists, campaigns, or any secret, and you choose how forms that already exist are treated: overwrite them, add only what's missing, refuse on any conflict, or overwrite only when the package is newer. A single CampaignCanvas:PackageApi:Token that opens both endpoints still works, but separate tokens are safer.
  • Security — exports no longer carry secrets or destinations, and imports are checked. An export used to include each form's captcha secret key and its workflows' credentials. It now leaves those out, together with the webhook, Slack, Teams and Discord URLs and the To/Cc/Bcc addresses of email workflows — they decide where submissions are sent, and they differ per environment anyway. Importing keeps the destination's own values for forms that already exist, so an import can't re-point a webhook (which keeps sending its own secret header) at another host. Forms an import creates start with those fields empty, and the import result lists exactly which workflows and captcha settings still need filling in. That also means an export is no longer a full backup of those fields. Imports now validate each form's alias and captcha site key (letters, digits, - and _ only) and reject the whole package, importing nothing, if one is off, if an alias is listed twice, or if an entry is empty; a request with no package, or with a conflict strategy this version doesn't know, is refused too instead of being read as an empty or an overwrite import. A new form whose id collides with a different existing form is imported under a fresh id instead of overwriting it, and when an overwrite changes a form's captcha provider or site key, the import result reminds you to check that the form's secret key still matches.
  • Security — captcha now refuses submissions it can't verify. A form with a captcha provider selected but no secret key (neither on the form nor in appsettings.json) used to log a warning and accept every submission, which quietly switched the protection off — notably for a form created by an import, since secret keys aren't part of a package. Those submissions are now rejected (visitors see the usual "captcha verification failed" message) and the application log names the missing key, so add the key or turn the captcha off on that form. The same goes for a form that names a captcha provider this install doesn't have, and for the newsletter signup form when its provider has no secret key. The site key is also encoded properly where it is written into the page.
  • Security — webhook calls are better protected against server-side request forgery. A webhook URL is now checked against a much wider list of non-public addresses — loopback, link-local (where cloud metadata services live), carrier-grade NAT, documentation and benchmarking ranges, multicast, and the IPv6 equivalents, including IPv4 addresses wrapped in IPv6 — and the address the connection actually uses is checked as well, so a host name that answers differently the second time can no longer steer a request to an internal server (if you route webhook calls through an outbound proxy, the proxy's own rules apply from that hop). Webhooks also no longer follow redirects: a URL that answers with a 301 or 302 now shows as a failed run in the form's Activity log, so point it at the final address. Webhook calls no longer keep cookies between requests. CampaignCanvas:AllowPrivateWebhookTargets (development only) still lets you reach http://localhost.
  • Security — throttling public form submissions needs two settings in your host. CampaignCanvas ships no FormSubmission rate-limit policy and doesn't switch the limiter on, so a site only throttles submissions once its Program.cs registers a policy with that name and calls UseRateLimiter() after routing. On an Umbraco host that is inside UseUmbraco().WithMiddleware(...); a call placed before UseUmbraco() does nothing. The snippet is in Getting Started.

1.2.8

  • New — send a Razor partial as a form email. The form workflow catalogue now includes Razor email. Pick any partial from Settings → Partial Views, and on submit CampaignCanvas renders it to HTML and sends it to the addresses you type and/or the form's email field. The partial must @model CampaignCanvas.Workflows.RazorEmailViewModel; use Model.Field("alias") to insert submitted values. A missing view or empty render shows as a failed run in the form's Activity log rather than mailing a blank body. Block List dispatchers such as singleblock/default.cshtml are not email templates and will fail if picked — write a dedicated email partial.
  • New — attach uploaded files to email notifications. The Email notification workflow has Include uploaded files as attachments. When it is on, every file the visitor uploaded on that submission is attached to the mail. Workflows now wait until those files are stored, so the attachments are actually there. Missing files are skipped so the message still goes out. Existing notifications stay as they were (the toggle starts off).

1.2.7

  • New — optional group titles on the form. You could name a group in the builder, but that name never appeared on the live form. Groups now have a Show group title on form toggle (off by default, so existing forms stay unchanged). When it is on, the group label renders as a heading above the group's fields.
  • Fix — Help text now shows on every input field. Help text is for visitors, not developers. It already appeared under text, email, long text, checkbox, rating, file, and mailing-list fields; it was missing on number, date, dropdown, radio, checkbox list, and consent. Those types now render the same small line under the control. Placeholder stays the grey hint inside the input.
  • New — localhost webhooks in development. Outbound form webhooks still refuse HTTP and private IPs in production (SSRF protection). Set CampaignCanvas:AllowPrivateWebhookTargets to true in a development appsettings file to send to http://localhost and other private addresses while testing. Leave it off everywhere else.
  • New — hCaptcha on forms. The captcha picker now includes hCaptcha alongside Cloudflare Turnstile and Google reCAPTCHA v3. Set CampaignCanvas:HCaptchaSiteKey and CampaignCanvas:HCaptchaSecretKey in appsettings.json, choose hCaptcha on the form, and the checkbox widget is rendered and verified the same way Turnstile already is. Existing Turnstile and reCAPTCHA forms are unchanged.
  • New — field widths of a third, a quarter, and two-thirds. The form builder could only lay fields out as full-width or half-width, so a row of three short inputs (title, first name, last name) or four (day, month, year, plus a code) had to be stacked or squeezed into halves. Each field now has Full, Half, Two-thirds, Third, and Quarter. Two-thirds sits next to a third on the same row. Forms you already built are unchanged: full and half still occupy the same space they did, and on a phone everything still stacks to one column at the same 560px breakpoint as before. Thirds and quarters step down to half-width on a narrower form (for example in a sidebar) so they don't become unusably small.

1.2.5

  • Fix — running out of Vouch credits mid-clean wrote off the addresses it never got to. When a Clean list run exhausted your credits partway through a batch, the addresses Vouch had accepted but never actually attempted were recorded as though they'd been checked and come back inconclusive. The run itself halted correctly and told you credits had run out — but topping up and re-running then skipped those very addresses, because they now counted as already-verified, leaving the opt-in "also re-verify previously-inconclusive addresses" checkbox as the only way to reach them. Addresses a credit-exhausted batch never attempted are now left unverified, so a re-run after a top-up carries on exactly where the last one stopped and every address in scope still gets a real verdict.

1.2.4

  • Trigger your own automations on contact activity, straight from Umbraco's Webhooks. CampaignCanvas now adds three events to Umbraco's built-in Settings → Webhooks picker — Contact Created, Contact Subscribed to List, and Contact Unsubscribed from List — so you can POST contact activity to Zapier, Make, a CRM, or your own endpoint with no code and no form to hang a workflow on. A subscribe fires the moment a contact becomes genuinely active — immediately on single-opt-in lists, or on the confirmation click for double-opt-in — and an unsubscribe carries the reason where one was given. Umbraco handles the delivery, retries, custom headers, and logging you already know.
  • Every subscribe and unsubscribe payload includes the contact's full list picture. Alongside the contact's details and the list that triggered the event, the payload carries a lists array of every mailing list with that contact's status on it — Subscribed, PendingConfirmation, Unsubscribed, Bounced, or None when they're not a member — so the receiving system can mirror the complete subscription state in a single call instead of asking back. Bulk imports and bulk list changes deliberately don't fire these per contact, so a 50,000-row import can't storm your endpoint.

1.2.3

  • Contact CSV export now records each address's verification status. The Export contacts to CSV download gains a VerificationStatus column, so every exported contact carries its latest Vouch verdict — Deliverable, Undeliverable, Risky, or Unknown for addresses not yet checked — right alongside the existing bounce columns. It makes list health easy to filter, report on, or hand to another system without opening the backoffice, and it's populated for suppressed contacts too so you keep the complete picture.

1.2.2

  • Re-verify addresses Vouch couldn't reach a verdict on. Some addresses come back inconclusive ("Unknown") from a clean — the mailbox server gave no clear answer, usually a temporary block, greylisting or timeout rather than a genuinely bad address. These are skipped on later cleans so you're never charged twice, but that left no way to try them again. The Clean list dialog now offers an opt-in "Also re-verify N previously-inconclusive addresses" checkbox: leave it unticked to verify only never-checked contacts as before, or tick it to re-check the inconclusive ones too. The estimated cost and credit check update to match your selection, so you always see the exact charge before starting — nothing is ever re-verified silently.

1.2.1

  • Fix — Clean list over-counted the contacts it would verify, and the credits it would cost. The pre-flight estimate counted your whole contact base instead of only the addresses still needing a check, so a mostly-clean list quoted a charge for everyone — a list of thousands could report thousands of credits (and warn you were short) when only a few hundred addresses were actually unverified. Already-verified contacts are now correctly skipped in both the estimate and the run itself, so the cost shown matches what you're charged, and re-running Clean list never re-verifies — or re-bills for — an address that was already checked on an earlier run.

1.2.0

  • New — know exactly who you're mailing, with email verification by Vouch. Every mailing list quietly rots — people change jobs, abandon inboxes, mistype their address on signup — and every dead address you keep mailing chips away at your sender reputation until real subscribers stop seeing you. CampaignCanvas now plugs straight into vouch.fast, which verifies addresses at the actual mailbox: a live SMTP conversation with Gmail, Outlook and the rest, not just a syntax pass. Setup is one paste — grab a vch_live_… key from the Vouch dashboard, drop it into Subscribers → Email verification, and it's validated and stored in your site's appsettings.json for you. Live instantly; no restart, no config-file surgery.
  • Clean your whole list in one click. The new Clean list action verifies your entire contact base (or a single mailing list) with live progress in the backoffice. Every subscriber gets a verdict — deliverable, undeliverable or risky — plus the reason (mailbox not found, no mail server, disposable domain, …), the grid filters by verdict, and definitively dead addresses go straight to the suppression list so every future send skips them automatically. Cleaner list, fewer bounces, better inbox placement for every campaign that follows.
  • Start with a free list health check. Every Vouch key includes one free, no-credit health check — a structural diagnosis of your whole contact base that tells you how bad the rot is before you spend anything ("18% of your subscribers look undeliverable or problematic"), broken down by cause. Percentages only — the full clean is what names names. It's the fastest way to see what Vouch can do: mint a free key at vouch.fast and run it today.
  • Fair, transparent credits. Verification runs on prepaid Vouch credits — roughly a cent per address, cheaper in volume, no subscription. CampaignCanvas shows the estimated cost against your live balance before a run starts and halts safely if credits run out. It's also deliberately cautious with your data: an ambiguous result never marks an address undeliverable, "risky" addresses are never removed automatically, and if Vouch is ever unreachable your signups and sends carry on untouched.
  • Dependency update — MailKit 4.15.1 → 4.17.0. Updates the SMTP delivery library to pick up the fix for a published security advisory.

1.1.19

  • Fix — multi-choice fields showed System.String[] in submission emails and CSV exports. When a form had a checkbox-list or multi-select field (for example "which newsletters would you like?"), the new-submission notification email and the submissions CSV export printed the literal text System.String[] in that row instead of the options the person ticked — so you couldn't tell what they'd actually chosen. These fields now render as a comma-separated list of the selected values. Single-value fields are unchanged, and nothing was lost from existing data: every choice was already stored, so re-exporting an older submission now shows its values correctly too. (Notification emails already delivered can't be rewritten — the fix applies to submissions received from this version onward.)

1.1.18

  • Bounced addresses are now suppressed automatically, however the bounce is reported. When a mailbox permanently rejects a message, that address is counted against its hard-bounce tally and — once it crosses the threshold (3 by default) — added to the suppression list, so it's skipped on every future send. Previously this only happened when your provider rejected the message at the moment of sending; bounces reported a few minutes later over a provider webhook — the usual case for SendGrid, Mailgun, Postmark and Resend — appeared in analytics but never stopped the address being mailed again. Continuing to email dead addresses quietly erodes your sending reputation, so this protects deliverability for your whole domain.
  • Hard and soft bounces are now told apart for every provider. SendGrid and Resend reported all delivery failures as a single generic "bounce", mixing permanent failures (a bad address) with temporary ones (a full mailbox, a greylisting deferral). They're now classified the same way Mailgun and Postmark already were — only genuinely permanent failures count toward auto-suppression, and your per-campaign hard/soft bounce breakdown is accurate.

1.1.17

  • Unsubscribes are now counted against the campaign that drove them. Opting out via the unsubscribe link or the preferences page now registers on the originating mailing's "unsubscribed" figure. Previously a campaign only counted the opt-outs your email provider reported back, so unsubscribes through the built-in link and preferences form were invisible in per-campaign analytics. The opt-out link now carries the send it came from, and repeated one-click hits (mail-client prefetch plus the manual confirm) are de-duplicated so the count stays accurate.
  • Email a list owner when someone unsubscribes. Every mailing list can now hold one or more notification addresses (Mailing list editor → Notifications). When a contact unsubscribes through the public link, those addresses receive an email naming the contact, the list, and any reason given; leave the field blank to keep it off. The preferences form could already notify via its email-notification workflow — this brings the same alert to the direct unsubscribe link, which has no workflow to hang one on.

1.1.16

  • Marketplace listing refresh. The package description and README have been rewritten into clearer, benefit-led copy covering the full forms + newsletter platform, with extra discovery tags (newsletters, email-marketing, gdpr). No functional changes — this release exists to publish the updated listing.
  • Docs — corrected licence-key link. The Licence key section pointed at campaigncanvas.net for installation instructions; it now points at the Datapad customer portal (projects.datapad.nl/projects/campaigncanvas), matching the purchase and support links.

1.1.15

  • Fix — an expired trial could still display as "Active". The License page and banner decided "active" from a cached flag without re-checking the expiry date, so a lapsed trial could show "Active" next to "Expired". An expired trial now correctly reads as inactive — so the genuine licence state, and any activation issue behind it, is no longer hidden.
  • Clearer activation failures. Entering a licence key that can't be validated no longer silently reverts to the trial. The page now states the reason — and an unlicensed domain reads "Domain 'X' isn't on this license yet. Add it in the portal at projects.datapad.nl, then click Refresh." instead of a generic message.
  • Diagnostics. When a validation response is rejected as outside the freshness window, the log now records the response's issuedAt and this server's current time, so a clock / time-sync problem on the host is obvious at a glance.

1.1.14

  • Maintenance. Licensing reliability and internal housekeeping.
  • Backoffice. The CampaignCanvas dashboard now shows the installed package version, so it's easy to confirm which build a site is running.
  • Licensing. Licence validation now reports the running Umbraco and CampaignCanvas versions to the licensing service, so compatibility guidance stays accurate.

1.1.13

  • Fix — newsletter settings page on SQL Server. The migrations that add the preferences-link and subscription-pages columns to the newsletter settings row used ALTER TABLE ... ADD COLUMN ..., which is valid SQLite syntax but invalid on SQL Server (SQL Server's ALTER TABLE ADD does not accept the COLUMN keyword). On SQL Server installs upgrading from 1.1.11 or earlier, the migration silently failed inside the startup upgrader and the newsletter settings page then errored with Invalid column name 'includePreferencesLink' / 'preferencesPageUrl' / 'subscriptionPagesJson'. The migrations now emit the keyword-free ALTER TABLE T ADD col TYPE form, which is valid on both providers. Every other ADD COLUMN migration in the plan got the same correction so future upgrades from intermediate versions don't hit the same trap.

1.1.12

  • Preferences link in campaigns. Outbound campaigns can now carry a "manage your email preferences" link in the footer alongside the existing unsubscribe link. Enable it from Newsletter settings and point it at a content page that hosts a preferences form — the link is signed per-subscriber, so the destination form consumes the token and writes back to the right subscription record.
  • Mailing-list checkboxes form field. New field type that renders one checkbox per configured mailing list and stores the ticked ids on the submission. Paired with the existing "Add to mailing list" workflow so a single form can opt a submitter into several lists at once, with labels captured at edit-time so renames don't break public pages.
  • Resubscribe after unsubscribe. The unsubscribe-success page now offers a one-click resubscribe path back into the same list. The link is a signed, single-purpose token (separate from the unsubscribe token) so an accidental unsubscribe is reversible without re-confirming via double-opt-in.
  • Editable copy for lifecycle pages. Every subscriber-facing page served by the newsletter controller — confirm-success, unsubscribe form + success, resubscribe form + success, invalid-token — now has editable heading / body / button-label slots in Newsletter settings. Blank slots fall through to the built-in defaults so editors can override only the strings they care about. The {listName} placeholder is substituted at render time.
  • Fix — emails referencing https://localhost. Logos, unsubscribe, preferences, open-pixel, and click-redirect URLs in dispatched campaigns now resolve to your public host. The background send worker has no HTTP context to derive a base URL from, so the public URL builder now falls back to Umbraco's WebRouting:UmbracoApplicationUrl configuration. Configure Umbraco:CMS:WebRouting:UmbracoApplicationUrl in appsettings.json for this to take effect — see Configuration.

1.1.11

  • Licensing. Licence validation now talks to the Datapad Licensing service (projects.datapad.nl). All purchase, support, and "manage your licence" links route through the customer portal at projects.datapad.nl/projects/campaigncanvas.
  • Hardening. The on-disk licence cache stores the raw signed envelope and verifies its Ed25519 signature on every load. Tampered cache files are discarded; legacy plaintext caches are migrated transparently.
  • Backoffice. Swept deprecated Umbraco backoffice APIs (auth.getLatestToken(), UmbPropertyValueChangeEvent) so the package stays compatible with upcoming Umbraco releases.

1.1.10

  • Fix. Custom Block Grid components in newsletters now render in the spot you placed them. The newsletter dispatcher sets a CampaignCanvas.IsNewsletterRender ViewData flag so host partials can emit table-row markup (<tr><td>…</td></tr>) for the email's outer table while keeping their normal page markup elsewhere. Previously, partials that emitted non-table HTML were hoisted out of the email's outer <table> by the browser and appeared above the email card.

1.1.9

  • Fix. Trial countdown now persists across app restarts and the 24-hour licence-cache refresh, so the banner correctly ticks down from 14 days instead of re-anchoring to "now" on every refresh.
  • Hardening. The persisted trial-state file is HMAC-signed, so manual edits to the start date are detected and the trial falls back to unlicensed instead of silently extending.