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
- Save a Turnstile (or hCaptcha) integration with its site key and secret key.
- Save the brand setting
newsletterwith 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_urlandunsubscribed_url(https pages shown after confirm and unsubscribe). - Switch the module on:
PUT /v1/manage/modules/newsletterwith{"enabled": true}. Without a working captcha the answer is 422captcha_not_readywith the reason.
Get a challenge and the captcha settings
/v1/public/newsletter/challengeAnswers 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
/v1/public/newsletter/subscribe{"contact": "0812 3456 7890", "locale": "id", "source": "footer",
"tags": ["promo"], "consent_version": "footer-2026-11",
"challenge": "<token>", "solution": 4711, "captcha_token": "<turnstile token>", "website": ""}| Field | Rule |
|---|---|
contact | Required. An email, or a phone number (0812..., +62812..., 62812..., 812...); an Indonesian number must be a mobile |
source | Required: footer, checkout, account, form or popup |
consent_version | Required: a version from the newsletter setting |
locale | Optional brand locale |
tags | Optional, at most 10; unknown keys are dropped |
challenge, solution | Required: the challenge token and the proof of work answer |
captcha_token | Required: the Turnstile (or hCaptcha) token |
website | Honeypot, leave it empty |
| Status | Body |
|---|---|
| 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 |
| 403 | preview_token_refused for a preview token, or a suspended site |
| 404 | The 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?