Newsletter

From a subscribe box on your site to a confirmed subscriber, with proof of consent.

3 min readUpdated Nov 11, 2026

Off by default

The newsletter module is off for every brand. An admin switches it on under Settings, Modules, and only after a Cloudflare Turnstile or hCaptcha integration is saved and its key test passes.

How it works

Your site's server posts one field, contact, to ATLAS. An email gets a confirmation mail and becomes a subscriber only when the person clicks the link (double opt in). A WhatsApp number is kept with its consent and nothing is sent to it yet. Every mail carries a one click unsubscribe link, and leaving takes effect at once.

Call the endpoints from your server with the brand's read token, never from the browser, and pass the visitor's address and user agent in X-Atlas-Client-Ip and X-Atlas-Client-User-Agent.

Set up

  1. Save a Turnstile (or hCaptcha) integration with its site key and secret key.
  2. Save the brand setting newsletter with at least one consent text: {"consent_texts": {"footer-2026-11": {"text": {"id": "...", "en": "..."}, "policy_url": "https://..."}}}. Optional keys: tags (the tag keys a site may send), channels (email, whatsapp), confirmed_url and unsubscribed_url (https pages shown after confirm and unsubscribe).
  3. Switch the module on: PUT /v1/manage/modules/newsletter with {"enabled": true}. Without a working captcha the answer is 422 captcha_not_ready with the reason.

Get a challenge and the captcha settings

POST/v1/public/newsletter/challenge

Answers the Shield challenge (token, min_seconds, expires_in, pow) and the captcha widget settings in captcha: provider, site_key, action (always newsletter_subscribe), mode, theme, size, appearance. Solve the proof of work (the number whose SHA-256 with salt equals challenge) and render the widget with that site key and action. A site server can also read data.newsletter.captcha from GET /v1/public/shield at boot.

Subscribe

POST/v1/public/newsletter/subscribe
jsonjson
{"contact": "0812 3456 7890", "locale": "id", "source": "footer",
 "tags": ["promo"], "consent_version": "footer-2026-11",
 "challenge": "<token>", "solution": 4711, "captcha_token": "<turnstile token>", "website": ""}
FieldRule
contactRequired. An email, or a phone number (0812..., +62812..., 62812..., 812...); an Indonesian number must be a mobile
sourceRequired: footer, checkout, account, form or popup
consent_versionRequired: a version from the newsletter setting
localeOptional brand locale
tagsOptional, at most 10; unknown keys are dropped
challenge, solutionRequired: the challenge token and the proof of work answer
captcha_tokenRequired: the Turnstile (or hCaptcha) token
websiteHoneypot, leave it empty
StatusBody
202{"data": {"channel": "email", "next": "confirm_email"}} or {"data": {"channel": "whatsapp", "next": "none"}}. The same for a new, a known and a blocked address
422{"message", "code", "errors"}: contact_invalid, channel_unavailable, consent_version_unknown, challenge_required, challenge_invalid, challenge_expired, challenge_failed, challenge_used, too_fast, captcha_required, captcha_failed
503{"message", "code": "captcha_unavailable"} with Retry-After: 60: the captcha provider could not answer or refused the keys. temporarily_unavailable with Retry-After: 60: the server is not set up for the newsletter yet. Either way nothing was stored and no mail was sent
429{"message", "code": "too_many_attempts"} with Retry-After
403preview_token_refused for a preview token, or a suspended site
404The module is off or the site is unknown

After every answer, fetch a new challenge and reset the captcha widget: both are single use.

Confirm and unsubscribe links

The confirmation mail links to /v1/newsletter/confirm/{token} (valid 72 hours) and every mail carries List-Unsubscribe and List-Unsubscribe-Post headers pointing at /v1/newsletter/unsubscribe/{token}. Opening a link shows a page with one button and changes nothing; the button (a POST) confirms or unsubscribes. Mail clients use the one click POST directly. With Accept: application/json the answer is {"data": {"status": "subscribed"}} or {"data": {"status": "unsubscribed"}}; an unknown or expired link is 404 link_expired.

Manage subscribers

Under /v1/manage/newsletter: GET subscribers (filters status, source, channel, exact address search), GET subscribers/{id}, GET summary. Every member reads them; editors see masked addresses, publishers and admins full ones. An admin can POST subscribers/{id}/suppress and DELETE subscribers/{id} (erase now). Unsubscribed data is erased after 7 days, unconfirmed sign ups after 14 days (WhatsApp numbers after 30), and the address hash stays on the suppression list.

Was this page helpful?

Suggest a change