Email and notifications

Updated Oct 16, 2026 Open the tool

Zoov.dev contacts users two ways: email (marketing campaigns you can opt out of, plus a few transactional messages) and Discord direct messages from the Zoov.dev bot for events on your account. This doc lists what is sent and how to stop it.

Marketing emails

Marketing emails are campaigns sent by staff to users who have marketing enabled. They come from "Zoov.dev updates@updates.zoov.dev" with replies going to support@zoov.dev. Every marketing email ends with "You are getting this because you created a Zoov.dev account and opted in to marketing emails." and an "Unsubscribe" link, and carries a one-click unsubscribe header so mail apps can show their own unsubscribe button.

One kit for every email (2026-10-16). Every email is rendered by one kit (kit.ts under src/lib/app/server/marketing-email): the workspace's own panel on the slate ground, a masthead with the mark and the name, a stage at the head with the headline, one paragraph, the buttons and a framed screenshot, the body as the workspace draws it (rows with a name, a line and a figure at the right; a callout on the brand wash; an offer with the code in a dashed line; a plan card wearing its plan colour; numbered steps; a review verbatim), and a footer under a hairline. Welcome for Roblox, Credits unspent and First pack carry pictures of their own (static/assets/email/shot-*.jpg, scripts/marketing/art/email-lifecycle.mjs: the landing's miniature of the ER:LC livery tool, the generated crate in its light, and the plans dialog's first two packs, photographed from the product and never drawn for the email). The light behind a headline is the landing's own light field, baked to three pictures (/assets/email/stage-brand.jpg, -ai, -ok, scripts/marketing/art/email-stage.mjs); account mail (a failed payment, a competition win) has no light. Every piece writes its plain-text twin, so the text part says what the HTML says. Phone first: 600px wide, 26px headline and full-width buttons under 600px, and every layout holds where a mail client drops the stylesheet. Dark on purpose: 79% of the list is Gmail, which shows a dark email as sent, and the workspace the button opens is dark. Before this there were three looks (the legacy purple shell, the workspace launch's kit, the Packs page's Line Sheet); the Packs beta mail keeps its own and the competition sends keep their shell until they are moved. The theme ids the scripts and the bot pass (graphite, ember, aurora) are accepted and ignored.

The template catalog (catalog.ts; every one is in the gallery at /ops/emails, see "Seeing every email"):

Template When Sent by
Welcome (welcome) the day an account is made; FiveM, or Roblox with --game roblox the lifecycle cron (below), or pnpm marketing:email --template welcome
Credits unspent (nudge) two days in, the 50 sign-up credits untouched: what they make, from the price table the lifecycle cron
First pack (first-pack) a week in, credits spent, never paid: the Starter pack as the offer (the $1.99 first-pack arm is the offers engine's to deal, never promised by mail), the pack ladder, the Standard card the lifecycle cron
Plan ended (lapsed) a subscription has ended: the files are still there, the plan that ended as a card (--tier), a code when staff pass one the lifecycle cron, the day the subscription ends
Win-back (inactive-comeback) no visit in a while: what shipped since (WHATS_NEW in catalog.ts), the tools and their free allowance, the Standard card, a first-month code (--promo-code, WELCOMEBACK by default) pnpm marketing:email --template inactive-comeback --days-inactive 30
Campaign (custom-topical) whatever staff draft with the bot's /email: the stage, the sections, an offer with the code, one closing action Discord /email → email-campaign.yml
Tools digest (tools-recap) every tool and what a run costs a free account, FiveM then Roblox, and what shipped lately pnpm marketing:email --template tools-recap --include-subscribers
Workspace launch (workspace-launch) sent 2026-09-22 to 23,537 people; kept for the record, on the kit pnpm marketing:launch-workspace
Packs beta (packs-beta) sent 2026-09-27 to 23,918 people; its own look pnpm marketing:launch-packs
Competition (competition) four audiences by four moments; its own shell pnpm competition:email
  • Workspace launch (2026-09-21), in detail: announces the new workspace at /app. It lists the tools with what each costs a free account, what AI does and what else is new (seven languages, plugin 1.6, search, monthly credits). It is the first email in the workspace's own look (dark panel, the Zoovdev mark, screenshots of the workspace) rather than the older purple one. Copy follows the landing's register, the one exception VOICE.md allows: whole sentences, benefit first, headlines that are noun phrases rather than imperatives, and no slogans (rewritten 2026-09-21 from a first draft that opened "Meet the new Zoovdev workspace" and said "we rebuilt").

    It is written four times, once per audience, and each person gets exactly one. The split is the same getSubscriptionSegments the competition sends use; it began as two, subscribers and everyone else, which told a person who paid for a year and a person who never paid the same thing.

    Audience Subject What is theirs alone
    subscribers The Zoovdev workspace is now available Their own plan named, and what its monthly credits became: "Your Pro plan now includes 1,500 credits every month", with "That is an increase from 1,000, with no change to the price. The new amount applies from your next renewal." Their plan and credits carry over and old links open the matching tool.
    cancelling The Zoovdev workspace is now available The same credit line, plus that the plan is set to end (cancelled at period end, or past due) and that nothing made is lost. A "View plans" button to /app?plans=all.
    lapsed Zoovdev has been rebuilt as a single workspace The whole credit ladder, before and after (Standard 500 to 750, Pro 1,000 to 1,500, Max 8,000 to 15,000), and that their files, projects and folders are still on the account.
    prospects One workspace for every Zoovdev tool What a free account includes. Nothing about credits going up: nothing of theirs did.

    The credits line needs the reader's own plan, which getSubscriptionSegments returns as planOf. That is deliberately not tierOf, the one the competition uses to hand out entries: tierOf counts only a live row, because a lapsed payment should not earn entries, and reading the launch email off it named nobody at all in the cancelling segment — that bucket is almost entirely past due rather than cancelled at period end (0 of 92 on 2026-09-21, 86 of 92 after). A live row always beats a past-due one, because an upgrade leaves the old plan behind as a past-due row and first-seen would otherwise name the plan someone has left.

    Three paying readers still have no plan to name and read the ladder instead: a team seat (no personal plan), a Plugin Pass holder (the plan grants no monthly credits, so "your credits went up" would be false) and anyone whose row could not be read. A dry run prints the split per segment ("Credits line: 86 named, 6 on the ladder | pro 60, standard 24, max 2"), so it is visible before a send. Before and after numbers come from monthlyCredits and PREVIOUS_MONTHLY_CREDITS in config/plans.ts, never retyped into the email.

Links inside marketing emails carry utm_source=email tags and are click-tracked. Marketing email is on by default for every account.

Transactional emails

Transactional emails are sent regardless of the marketing setting because they concern your account or money. The ones in code:

  • Competition winner: "You won in the " (cash or product prizes add "Action required" and must be answered by reply within the claim window, otherwise the prize is forfeited). Credit prizes are already on the account. Since 2026-10-16 it is on the kit (winner.ts): the prize as the headline, the three steps to claim a cash or product prize with the date in a callout, Reply to claim and Competition terms; a credit prize says the credits are on the account and offers Open workspace. Before, it was an unstyled paragraph.
  • Failed subscription payment (dunning): "Your zoov.dev payment did not go through", with "Pay or update card" and "Manage billing" buttons pointing at /api/v2/billing/portal?return=/app. Sent only when staff run the dunning script by hand with --email; the daily dunning pass charges cards and never emails. "Pay or update card" opens Stripe's own invoice page when there is one. Since 2026-10-16 it is on the kit: no light, the fact as the headline, the two buttons, "Nothing is lost" in a callout, the invoice line small.

Sign-in does not send email; accounts use Discord, so there is no verification or password email. Stripe may send its own receipts and invoices separately (verify in the Stripe dashboard settings). The /unsubscribe page says the same: "Account and purchase emails still arrive."

Managing email preferences

Since 2026-09-27 the switch is on the profile. /app/profile has one row under the plan, credits, storage and team facts (the storage fact's expiry wording changed on 2026-10-02, see profiles-and-verification; nothing about email did): Marketing emails, with a switch. Off means no campaign reaches the account; on means they do. The switch saves as it is moved (PATCH /api/v2/me/email { marketing }, a toast "Marketing emails off" or "Marketing emails on"); if the save fails the switch comes back and the toast reads "Could not save · try again". The row it changes is email_preferences.marketing, the flag every campaign send reads at send time. The address the emails go to is the one in the identity line at the top of the page (the account's sign-in email). Account and purchase email (receipts, sign-in codes, competition results, failed-payment notices) is not a choice: updates and transactional exist on the row and nothing sends by them.

The old page /email-preferences (the legacy site's, with its "Receive marketing emails" toggle and a template catalog for previewing campaigns) is deleted; the address redirects to /app/profile (LEGACY_TO_APP in src/lib/config/release.ts). The support bot's path list names the profile for anything about emails, unsubscribing or newsletters (src/lib/config/app-paths.ts).

The unsubscribe link is /unsubscribe?token=<your token>, in the foot of every marketing email and in its List-Unsubscribe header. It works without signing in, from any device or a forwarded email: the token is a per-account random value that never expires and is the same in every email that account receives. Since 2026-09-27 the page is on the app's design (the (pages) group, the same bar and footer as the guides and the policies; before, it was a legacy-site page): the title "Unsubscribe", one line ("Marketing emails from Zoovdev stop. Account and purchase emails still arrive."), a row "Marketing emails · On" or "Off", and one button. Unsubscribe turns marketing off and the page then reads "Off" with the status "Unsubscribed" and the button Turn back on, which is new: a press that was a slip is undone on the same page, and the page reads "Turned back on". A link at the foot, "Email settings in your profile", goes to /app/profile. A missing or wrong token reads "This link is not valid. Email settings are in your profile." A save that fails reads "Could not save · try again". The page is noindex.

One-click unsubscribe from mail clients works since 2026-09-27; it never did before. Every marketing email carries List-Unsubscribe: <https://zoov.dev/unsubscribe?token=…> and List-Unsubscribe-Post: List-Unsubscribe=One-Click, so Gmail, Apple Mail and the others show their own Unsubscribe button and, when it is pressed, POST the form field List-Unsubscribe=One-Click to that address from their own servers, with no Origin header (RFC 8058). SvelteKit refuses every form-encoded POST whose origin is not the site's before any hook runs, with "Cross-site POST form submissions are forbidden" (403), and has no exemption for one address, so every such press was refused (checked against production 2026-09-27). Now SvelteKit's own check is switched off (kit.csrf.trustedOrigins: ['*'] in svelte.config.js, the documented way; checkOrigin is deprecated) and hooks.server.ts makes the same test first in its chain (crossSiteForms, from src/lib/server/cross-site.ts: a POST, PUT, PATCH or DELETE with a form content type, application/x-www-form-urlencoded, multipart/form-data or text/plain, from an origin that is not ours, or with none, gets the same 403 SvelteKit gave), with one exception: a POST to /unsubscribe with a token in the address whose body is the RFC 8058 field. That request reaches the page's one form action, which reads the token from the address and, finding no marketing=on field, turns marketing off, answering 200 with the page. The hook runs in dev too, where SvelteKit skipped its own check, so the probe (node_modules/.probe/unsubscribe.mjs) exercises it: the one-click POST answers 200 and the row reads off, a form POST from another origin answers 403 and changes nothing, the site's own answers 200, and a one-click POST with no token answers 403. Unit tests: src/lib/server/cross-site.test.ts.

When a person asks how to stop the emails: the link at the foot of any marketing email, their mail client's own Unsubscribe button, or the switch on /app/profile. All three set the same flag, and any of them can turn it back on (the page's button, the profile's switch).

Discord DMs the site sends

Discord DMs come from the Zoov.dev bot in the Zoov.dev server. The site sends these:

  • 3D model ready / failed: "Your 3D model generation is complete!" (or "Your remeshed model is complete!") with "Your model is ready and saved to your library." and an Open it in the workspace link to /app/prop, and "Your 3D model generation failed." explaining any credit refund. Sent once per task. The ready DM goes out on Meshy's first SUCCEEDED delivery, before the model is stored (since 2026-10-13 the site first reads the task back from Meshy and acts on what Meshy says, not on the delivery); since 2026-10-12 a run whose model never gets stored is refunded 15 minutes later with no DM, only the Credits refunded bell (image-to-3d, "Automatic refunds"). Until 2026-09-23 the ready DM linked to /app/convert?to=ydr, which redirects to the Converter's YDR recipe, so it opened the wrong tool; and the failure DM carried the provider's raw wording while /app rewrote it. Both now say the same thing as the workspace, so a person quoting a DM and a person quoting the screen read alike.
  • Loyalty credits: "Thanks for staying with Zoov.dev, your loyalty credits are here" when a subscription streak milestone awards credits, with a "View My Credits" button.
  • Abandoned checkout: a recovery DM an hour after a checkout page is left (the session's life since 2026-10-06), headed "You left something behind", once per customer per 24 hours. Since 2026-10-06 the words know the cart ("You were a step from 400 credits ($12.99) and the page timed out. Nothing was charged."), say what the item is in the game the checkout's door belongs to (liveries for a Roblox door, textures and conversions for a FiveM one, the plan's credits and features for a plan), and offer a first purchase the code BACK15 (15% off) on a "Continue with BACK15" button that opens the workspace's plans with the code applied, beside "Same cart, no code" (Stripe's own recovery link); a returning buyer gets "Continue checkout" and, for a plan, "Compare plans". Footer: "You get this once because you started a checkout on zoov.dev." The copy lives in src/lib/server/payments/recovery.ts; see the billing doc. If Discord refuses the DM (closed DMs, not in the server) nothing is retried. Since 2026-10-02 the Stripe event is marked skipped rather than failed. Since 2026-10-10: a checkout paid inside the Credits dialog has no Stripe recovery page, so its DM links to the workspace's plans dialog on the packs; and no DM goes to someone who has paid for anything since the expired checkout began.
  • Payment issue: "Payment issue with your subscription" when a subscription becomes past due, unpaid or incomplete, with "Update Payment Method".
  • Competition entries: "You're in the prize draw: ..." when a subscription or credit package adds entries.
  • Feedback offer: after a feedback form with high upgrade intent, "Exclusive 20% off offer" (code FEEDBACK20) or "Exclusive one-time plugin offer". Sent only for the returning non-paying survey, which since 2026-10-01 the server accepts only from people eligible for it.
  • Teams: "Team Invitation" with an accept link (expires in 7 days) and "Welcome to !" after joining. A team's reactivation restores a returning member's seat credits once per leave (2026-10-01), whatever the number of reactivation calls.

There is no site setting to turn DMs off. DMs need your Discord privacy setting "Allow direct messages from server members" enabled for the Zoov.dev server; if you disable it the bot cannot reach you and the event simply goes unnotified. Logins and admin alerts go to staff channels, not to users.

Since 2026-10-05 the same events also land in the bell in the workspace's top bar (the Notifications doc): Pack invitation, Team invitation, Credits refunded (a 3D generation), You won and Payment failed, plus Referral reward, which never had a DM. The bell keeps them when a DM is blocked or an email is missed; it sends nothing by itself.

Common questions

How do I stop marketing emails? Click "Unsubscribe" at the foot of any marketing email (no sign-in needed), use your mail app's own unsubscribe button, or turn off the Marketing emails switch on /app/profile. It saves at once.

I unsubscribed but still got an email. Check whether it was transactional (competition win, failed payment); those are always sent. Marketing sends use the flag at send time, so an email already queued that day may still arrive. If a marketing email arrives days later, ask staff to check the account's preference row. Since 2026-09-27 the person can check it themselves: the Marketing emails switch on /app/profile.

I want marketing emails again. Sign in, open /app/profile and turn the Marketing emails switch on. If you pressed Unsubscribe by mistake, the same page has Turn back on.

Which email address do you use? The address on your Discord account at your last sign-in. Change it on Discord and sign in again.

Why did the bot DM me? The site DMs you about your own tasks, payments, credits, competitions and teams. It never DMs marketing campaigns. A referral reward (2026-10-01) sends no DM: it shows on /app/referrals and in the credits dialog as "Referral reward".

The bot cannot DM me. Enable "Allow direct messages from server members" for the Zoov.dev server in Discord privacy settings.

Credit logs arrive a moment later (2026-09-28)

The Discord logs of credit transactions (Credit Transaction, Credits Added, Credits Removed) and of image and tattoo generations are posted after the person's request has been answered, so they can land a second or two after the thing they record. A generation's result log still links to its input log.

Topics emailmarketingunsubscribenotificationsdiscord-dmresendpreferences

Something missing? Ask on Discord