Newsletter & subscribers
Capture email subscribers from your storefront. Signup is double opt-in:
your storefront submits the email, the platform queues a confirmation link for
delivery, and the subscriber becomes confirmed only after they click it.
Merchant endpoints let you read and export the tenant's subscriber list.
Examples reuse the api() helper.
Subscribe (public)
POST /api/v1/public/newsletter/subscribe — tenant header, no auth (it's a
public capture, rate-limited per tenant).
await api("/public/newsletter/subscribe", {
method: "POST",
body: JSON.stringify({
email: "dana@example.com", // required, valid email — normalized to lowercase
source: "footer", // optional free-form capture-origin tag
}),
});
// → { ok: true }A successful request returns 201 with { ok: true }. It deliberately
doesn't reveal whether the email was new, pending, confirmed, or previously
unsubscribed, so the result cannot be used to probe who's subscribed. Behind
that same response, the subscription states behave differently:
| Existing state | Effect of an explicit signup submission |
|---|---|
| No subscriber | Create a pending subscriber and queue a confirmation email. |
| Pending | Keep the pending subscriber and queue another confirmation email. |
| Confirmed | No change and no new confirmation email. |
| Unsubscribed | Return to pending, replace the confirmation token, and queue a new confirmation email. The subscriber must confirm again. |
Repeated submissions do not create duplicate subscribers, but they are not side-effect-free retries: a pending signup can queue another email. Do not repeat a successful POST to poll subscription state, or assume an uncertain response means the first request did nothing. A deliberate resend or renewed signup uses the same endpoint and keeps the same neutral success result.
Invalid input can return 400; tenant API quota exhaustion can return 429.
For that quota refusal, respect Retry-After before a further submission. The
constant success body does not replace those error responses.
Acceptance records the signup or resend intent, not confirmation or email delivery. Sending still depends on the environment's delivery policy, configured transport and recipient eligibility. A delivery failure does not turn an accepted signup into an HTTP error; the queued work can be retried separately. A subscriber who confirms or unsubscribes before delivery no longer needs a pending confirmation email.
The double opt-in flow
- A new signup or explicit resubscription → subscriber stored pending, confirmation email queued for permitted, configured delivery.
- The email contains a confirm link —
GET …/public/newsletter/confirm?token=…. The subscriber clicks it and sees a platform confirmation page after the server confirms the subscription. A repeat click shows that it is already confirmed. Continue to storefront opens the trusted return destination, when one is available. - Every email also carries a one-click unsubscribe link —
GET …/public/newsletter/unsubscribe?token=….
The confirm and unsubscribe links are tokenized targets the platform emails — they're not endpoints your storefront builds or calls. Your storefront uses the
subscribePOST for capture; the platform owns the confirmation + unsubscribe lifecycle (which is why those two routes aren't in the API reference).
Confirmed means the double opt-in link was accepted. It does not prove provider acceptance, inbox delivery, or permission to bypass delivery and suppression policies. Capture, confirmation, list reads, and exports do not provide campaign authoring, broadcast sending, or automatic ESP synchronization. See Email customization for the separate template, sender, and delivery boundaries.
Read subscribers (merchant)
GET /api/v1/merchant/newsletter/subscribers requires a merchant staff JWT and
the tenant header. All organization members can read the list; customer sessions
and server-only API keys do not authorize this merchant route. Run merchant
examples on your server, with token holding the staff JWT and the api() helper
supplying the tenant header.
| Query | Behavior |
|---|---|
filter | confirmed by default; also pending, unsubscribed, or all. |
search | Trimmed, case-insensitive substring search over email or source, combined with the lifecycle filter. Blank search adds no restriction. |
limit | Page size: default 50, minimum 1, maximum 200. |
cursor | The previous page's opaque nextCursor; omit for the first page. |
const query = new URLSearchParams({
filter: "confirmed",
search: "footer",
limit: "50",
});
await api(`/merchant/newsletter/subscribers?${query}`, { token });
// → { data: [...], nextCursor: "..." } (null at the end)Results are newest first. Each row includes id, email, status, source,
createdAt, confirmedAt, and unsubscribedAt. Status is PENDING, CONFIRMED,
or UNSUBSCRIBED; source, confirmedAt, and unsubscribedAt can be null. The
projection excludes confirmation and unsubscribe tokens.
For another page, pass the returned nextCursor as cursor, retaining the same
filter and search. Stop when nextCursor is null; do not interpret or construct
cursor values. The response contains data and nextCursor, not a total count.
Export subscribers (merchant)
GET /api/v1/merchant/newsletter/subscribers/export uses the same merchant JWT
and tenant header, and additionally requires customers:support. Owner/Admin
retain access; Staff need that permission group. A member who can view the JSON
list may therefore receive 403 for the export.
Pass the same filter and search values as the list; the default filter is
confirmed. Export streams all matching rows, with no row cap or pagination,
in newest-first order. A list page's limit or cursor does not bound it.
The response is a UTF-8 text/csv attachment with these columns:
email,status,source,createdAt,confirmedAt,unsubscribedAtDates use ISO timestamps; null values become empty cells. An empty result still returns the header row. The CSV excludes subscriber IDs and action tokens, and escapes cells that could be interpreted as spreadsheet formulas. The export is recorded in the audit log.
Use a server-side download request with the merchant credentials and stream its
response to a file or authorized download. The JSON api() helper above calls
res.json(), so use a CSV download path instead of passing this response to it.
Keep exported subscriber data out of public storefront responses.
Action destinations
The emailed confirm and unsubscribe links always validate the opaque token on cartonium's server before changing subscriber state. Exact persistent staging—the only Preview environment permitted to send newsletter email—uses the stable API Preview domain, never a raw Vercel deployment URL. Ordinary PR Previews build deployment-local action links but cannot send newsletter email.
Successful confirmation shows the platform acknowledgement first, without an automatic redirect. Its Continue to storefront link uses the trusted destination order below; if no destination resolves, the page has no Continue link. The link only navigates and never repeats a subscription or email action. Unsubscribe and inactive-link actions retain their existing trusted redirects.
| Action | Destination order |
|---|---|
| Confirmed or already confirmed — Continue link | postActionRedirectUrl → storefront origin → optional platform fallback → acknowledgement without a link |
| Unsubscribed | newsletterUnsubscribeRedirectUrl → postActionRedirectUrl → storefront origin → optional platform fallback → neutral page |
| Link no longer active | newsletterInactiveRedirectUrl → optional platform fallback → neutral page |
Configured destinations must be absolute http(s) URLs whose origin is the
tenant's trusted storefront domain, that tenant's hosted cartonium checkout origin,
or an operator-approved host. Blank settings are normal: they fall through to the
next documented destination. Invalid or untrusted settings are ignored rather
than emitted as a Continue link or browser redirect. Unknown or missing tokens
return the same neutral error and never provide a destination; a URL success
flag is not proof of confirmation. The storefront-origin fallback is resolved
from the tenant's canonical platform configuration. The optional platform
fallback is an operator-approved neutral destination; tenants need not configure
one or invent a success path. Inactive links never fall through to the shared
success destination or storefront origin.
Related
- Email customization — template controls and delivery boundaries
- Settings & onboarding — tenant profile the emails send under
- Abandoned checkouts and cart capture — hosted recovery in active development and legacy cart capture