Supercommerce API Docs
Admin API

Email Module — Admin

Operator-configurable email: outbound provider (Resend/SMTP), store brand tokens for transactional emails, per-template content overrides, and the block-based campaign builder.

Operator-configurable email: the outbound provider (Resend or SMTP), store brand tokens rendered into transactional/auth emails, per-template content overrides, and the block-based campaign builder.

Source: api-modules/email/src/controllers/*, settings registry api-modules/settings/src/registry/{admin/email.ts,store/email.ts,store/branding.ts}.

Configuration (settings)

All configuration is stored in the settings registry and edited via the standard settings UI (PUT /admin/settings, PUT /store/settings). The live mailer reads provider + brand at send time — changes apply on the next send, no redeploy.

Provider — admin.email

KeyTypeNotes
provider"smtp" | "resend"Active provider (default smtp).
from_emailstringSender address (verified on the provider). Empty → MAIL_FROM env fallback.
from_namestringSender display name (Name <email>).
reply_tostringOptional Reply-To.
resend_api_keystring (secret)Shown when provider = resend.
smtp_host / smtp_port / smtp_secure / smtp_user / smtp_passwordShown when provider = smtp. smtp_password is secret.

When the provider is unconfigured (blank host / key), the mailer falls back to the SMTP_* env vars, so local dev and fresh installs keep working. Both providers are backed by @opencoredev/email-sdk (its resend and smtp adapters).

Brand tokens — store.branding + store.email

Transactional and auth emails inherit these. Campaign emails do NOT — they are fully standalone.

KeyGroupNotes
accent_color, accent_text_colorstore.brandingHex; buttons/links.
logo_urlstore.emailOptional; falls back to store.branding.logo_url.
footer_linksstore.email{ label, url }[].
social_linksstore.email{ platform, url, icon }[]icon is an uploaded image (or pasted URL); renders as an icon in the footer.
footer_textstore.emailLegal small print / address.

The storefront URL used in emails is read from store.storefront_urls.store_url, falling back to store.seo.canonical_base_url when blank.

Auth emails link to the storefront (not the API). Each link is built as store.storefront_urls.store_url (falling back to store.seo.canonical_base_url) + the configured path + the better-auth token. Defaults match the store UI's routes; override per deployment.

KeyDefaultToken param
auth_reset_password_path/auth/reset-password?token=
auth_verification_path/auth/verification?code=
auth_magic_link_path/auth/magic-link?token=
auth_change_email_path/auth/change-email?token=
auth_delete_account_path/account-deleted?token=

If no store base URL is set, the email keeps better-auth's own server URL.

Endpoints

Provider test

POST /admin/email/test — send a test email to verify the active provider config. Permission: emailTemplate:test.

// body
{ "to": "me@example.com" }

Transactional template overrides

Every template — notification templates like order.placed, the guest-checkout order confirmation guest_order.confirmation (the email a guest gets with their tokenized tracking link), the vendor-application lifecycle emails (vendor.application.submitted, vendor.application.approved, vendor.application.rejected, role vendor_member), the back-in-stock emails (back_in_stock.available to the customer — the only channel that can reach a guest subscriber — and back_in_stock.vendor_alert to vendor_member, the operator-triggered restock nudge), and the auth/default emails (auth.reset_password, auth.magic_link, auth.verification, auth.change_email, auth.delete_account, auth.password_changed, auth.welcome) — is data-driven: it has an editable default subject / heading / body (HTML with {{variables}}). The editor shows the real defaults (prefilled, not blank); you edit the body directly. At send, variables are interpolated and the body is rendered inside the brand shell (logo header + footer). The legacy React templates remain only as a fallback for any template without a registered default. Auth emails always send (the enabled toggle is ignored for them — you can't disable e.g. a password reset).

Permission: emailTemplate:view (read), emailTemplate:update (write), emailTemplate:test.

MethodPathDescription
GET/admin/email/templatesList every registered template with its effective (override ?? default) subject/heading/body, enabled, hasOverride, available {{variables}}.
GET/admin/email/templates/:eventType/:roleOne template's effective content + variable hints.
PUT/admin/email/templates/:eventType/:roleUpsert the override. Empty fields fall back to the code default. enabled:false skips that email (ignored for auth.*).
POST/admin/email/templates/:eventType/:role/previewRender the current (unsaved) draft {subject, heading, body} to branded HTML with sample data; returns { subject, html }. Powers the editor's live preview.
POST/admin/email/templates/:eventType/:role/testRender this template with sample data + current brand and send it to an address.

Body/subject/heading support {{variable}} placeholders. Common variables (customer_name, store_name, store_url, support_email, accent_color) are always available; every top-level scalar field of the event payload is exposed in snake_case (money fields formatted, e.g. {{grand_total}}₹1250.00, with the raw value under {{grand_total_raw}}). Auth emails expose {{url}} (and {{new_email}} for email-change).

// PUT body
{
  "enabled": true,
  "subject": "Order {{order_number}} confirmed",
  "heading": "Thanks, {{customer_name}}!",
  "body": "<p>Hi {{customer_name}}, your order is on its way.</p><p>Total: {{grand_total}}.</p>"
}

Campaigns (block builder)

Campaigns ride the existing broadcast engine (audience resolution, worker fan-out, scheduling, status counters — see notifications.md). The body is built from safe blocks and rendered standalone (no transactional brand chrome).

Permissions: campaignTemplate:view (read), campaignTemplate:manage (write), notifications:broadcast (send).

MethodPathDescription
GET/admin/campaigns/templatesList saved campaign templates (paginated).
GET/admin/campaigns/templates/:idOne template.
POST/admin/campaigns/templatesCreate a template.
PUT/admin/campaigns/templates/:idUpdate a template.
POST/admin/campaigns/templates/:id/duplicateDuplicate.
DELETE/admin/campaigns/templates/:idDelete.
POST/admin/campaigns/previewRender blocks → HTML (sample vars + store logo) for live preview.
POST/admin/campaigns/sendSend (or schedule) a campaign to an audience.

Blocks: heading, text, image, logo, button, social, divider, spacer. Text/heading/button fields support {{variables}}. There is no raw-HTML block.

// POST /admin/campaigns/send
{
  "audience": { "type": "all_customers" },          // or { "type": "user_ids", "userIds": [...] }
  "subject": "Summer sale is live",
  "previewText": "Up to 40% off",
  "templateId": "…",        // OR provide "blocks": [...] inline
  "scheduleFor": "2026-07-01T09:00:00Z"   // optional; send-now if omitted
}

A templateId snapshots the template's subject + blocks at send time, so later edits never mutate an in-flight campaign.

On this page