# AtlasCMS --- # Blog Source: https://atlascms.id/blog Blog ## Notes from building ATLAS Product news, engineering write ups and release stories from the team behind the headless CMS for many brands. --- # Theme development Source: https://atlascms.id/docs/theme-development On this page ## Theme development From theme.json to a live brand: what a theme contains and how it gets into AtlasCMS. - 7 min read ### What a theme package is A theme turns a brand's blocks into a website. It is a versioned package with a `theme.json` manifest, default templates and parts written as block trees, block definitions for the blocks the theme adds, and a few images. The API stores a snapshot of the manifest and the per brand data (which theme runs, its options, template overrides). It never stores or runs theme code. A package you upload is data only. The API reads the zip, checks every entry and keeps the JSON. It does not execute or serve component code. The components that draw your blocks live in the site that renders the brand, built on the layer packages described in [@atlas/nuxt](/docs/nuxt). A typical package looks like this: ```text theme.json blocks/definitions.json templates/index.json templates/page.json parts/header.json parts/footer.json screenshot.png ``` ### The theme.json manifest The manifest sits at the root of the package. Every key below is checked on upload and by `php artisan themes:sync`. ```json { "manifestVersion": 1, "slug": "acme", "name": "Acme", "version": "1.0.0", "requires": { "api": ">=0.1.0" }, "blocks": { "prefix": "acme-" }, "templates": [ { "slug": "index", "title": "Fallback" }, { "slug": "page", "title": "Page" } ], "customTemplates": [ { "slug": "landing-no-header", "title": "Landing", "types": ["page"] } ], "parts": [ { "slug": "header", "area": "header", "title": "Header" }, { "slug": "footer", "area": "footer", "title": "Footer" } ], "menuLocations": [ { "key": "header", "title": "Main navigation", "maxDepth": 3 } ], "styleVariations": [], "tokens": {}, "options": { "type": "object", "properties": { "headerCtaLabel": { "type": "string", "maxLength": 40, "x-translatable": true } } } } ``` | Key | Rule | | --- | --- | | `manifestVersion` | Must be `1`. | | `slug`, `version` | A lowercase slug of at most 64 characters, and a SemVer version such as `1.2.0`. | | `blocks.prefix` | Required. Your block types must start with it. `atlas-` is reserved. | | `templates` | The templates the package ships. `index` is required, it is the final fallback. | | `customTemplates` | Templates an editor can pick for an entry. Each lists the entry `types` it applies to. The slug may not be one the template hierarchy already owns. | | `parts` | Reusable block trees, each with an `area` such as `header` or `footer`. | | `menuLocations` | Menu keys the theme renders. `maxDepth` is a whole number from 1 to 3. | | `styleVariations`, `tokens` | Token sets. A value may not point at a remote file (`//`, `://`, `url(`, `image-set(`). | | `requires.api` | A comparator list such as `>=0.3.0`. A package that needs a newer API is refused. | | `options` | The settings a brand may change, declared as JSON schema. | A manifest may not declare `scripts`, `head`, `routes` or `server`. A package uploaded by a brand may not use `extends`. ### Blocks and block definitions A block definition says which props a block accepts. It drives the editor form and the server validation of every page that uses the block. A definition is `{ "type", "version", "schema" }`, and the type must start with your `blocks.prefix`. ```json [ { "type": "acme-hero", "version": 1, "schema": { "type": "object", "required": ["heading"], "properties": { "heading": { "type": "string", "maxLength": 120 }, "image": { "type": "object" } } } } ] ``` You can load definitions in three ways: - Put them in `blocks/definitions.json` inside the package. Upload and `themes:sync` read them. - Run `php artisan blocks:sync definitions.json --brand=acme` on the server. Leave out `--brand` to save them as global definitions. - As an admin, call `PUT /v1/manage/block-definitions` with `{ "definitions": [ ... ] }` (1 to 200 items). A definition saved this way belongs to the brand. PUT Blocks nest three levels deep at most. The two names `atlas-template-part` and `atlas-post-content` are renderer boundaries: they need no definition and no write path accepts a definition for them. Until a brand has any definition, block validation is skipped. ### Templates and parts A template is a block tree chosen by the template hierarchy. For a path, the API lists candidates and the first one that exists wins. A page at `/tentang` tries `page-tentang`, `page`, `singular`, `index`. A post tries `single-{type}-{slug}`, `single-{type}`, `single`, `singular`, `index`. The locale root tries `front-page` first, the posts page tries `home`, `archive-post`, `archive`, and nothing found uses `404`. For each candidate the API checks, in order: - A published override of this brand for the requested locale. - A published override for the default locale, while the `locale_fallback` setting is not `false`. - The file the package ships. When the package file answers, the page payload says `"source": "package"` with `"blocks": null`, and the renderer uses its own copy. A part with no override carries the default blocks from the package. Editors change a template or part without touching the package. These calls need a **publisher**: | Call | Effect | | --- | --- | | `PUT /v1/manage/theme/templates/{kind}/{slug}/draft` | Saves `{ locale?, blocks }` as a draft. `kind` is `template` or `part`. | | `POST /v1/manage/theme/templates/{kind}/{slug}/publish` | Makes the draft live. 422 when there is no draft. | | `DELETE /v1/manage/theme/templates/{kind}/{slug}` | Drops the override so the package default answers again. | A draft is not live output. Publishing it audits the change, raises the brand content version and queues the [purge webhook](/docs/webhooks). `atlas-post-content` renders the entry body, so it is only allowed in a template that renders one entry, and once per tree. A part never holds it. ### Theme options and the settings registry AtlasCMS keeps settings in a registry: each core setting is declared once with a type, a default and the role that may write it. `GET /v1/manage/settings/definitions` lists them for any member. Theme options are a separate set. Your manifest `options` object is the only thing that decides which option keys exist, and `PUT /v1/manage/settings` refuses them. Options use the same JSON schema subset as block props, plus two markers: | Key | Meaning | | --- | --- | | `x-translatable` | The value is a map keyed by brand locale. A site gets the requested locale, then the default locale. | | `x-private` | The option never reaches a site. Only members see it in the dashboard. | | `format` | One of `uri` (checked as a safe link), `media` (a ready file of the brand), `color` (`#rrggbb`) or `html` (cleaned on save). | A `format: media` option is stored as a media id and delivered as `{ media_id, media: { url, mime, width, height, alt, focal } }`. A brand may have at most 100 options, and one value may be 8 KB at most. An empty value clears an option back to its default. Options have a draft and a live copy, so an editor can preview before publishing: | Call | Role | | --- | --- | | `GET /v1/manage/theme/options/{slug}` | Any member | | `PUT /v1/manage/theme/options/{slug}/draft` with `{ "options": { ... } }` | Publisher | | `POST /v1/manage/theme/options/{slug}/publish` | Publisher | | `DELETE /v1/manage/theme/options/{slug}/draft` | Publisher | The public `GET /v1/public/theme` returns the live options of the active theme. A request with a preview token receives the draft instead, and preview answers are never cached. ### Sync a theme on the server An operator with shell access can store a package directory or a bare manifest as a platform theme every brand can see: ```bash php artisan themes:sync themes/acme php artisan themes:sync themes/acme/theme.json --definitions=build/definitions.json ``` The command validates the manifest and one file per template and part. A bad file refuses the whole sync and writes nothing. It also syncs the block definitions. It stores a snapshot and never activates a theme. ### Upload a theme A brand admin can upload a package as a zip. The theme then belongs to that brand only. POST The body is `multipart/form-data` with the file in `package` and an optional boolean `dry_run`. Always run a dry run first: it runs the same checks and writes nothing. ```bash curl -s -X POST https://api.atlascms.id/v1/manage/themes \ -b cookies.txt -H "X-CSRF-TOKEN: $CSRF" -H "X-Brand-Id: $BRAND_ID" \ -F "package=@acme-1.0.0.zip" -F "dry_run=1" ``` ```json { "dry_run": true, "result": "would_create", "theme": { "slug": "acme", "name": "Acme", "version": "1.0.0" }, "counts": { "templates": 2, "parts": 2, "blocks": 1, "files": 8 }, "errors": [], "warnings": [] } ``` `result` is `would_create` on a dry run, then `created` or `unchanged` on a real upload. `unchanged` means the same slug and version with the same content is already stored. The rules of a package: - `theme.json` sits at the root of the zip. A single wrapper folder is not accepted. - Only `.json`, `.png`, `.jpg`, `.jpeg` and `.webp` files. No hidden files, symlinks or encrypted archives. An image must really be an image. - At most 5 MB zipped, 200 files, 2 MB per file and 20 MB unpacked. A brand holds up to 10 themes and 20 versions of each. - The slug may not belong to a platform theme, and the `blocks.prefix` may not collide with a global block. - A version is immutable. The same slug and version with different content answers **409** `version_exists`: raise `version` in the manifest. A package with any error answers **422** with the same shape and `errors` filled, each item `{ file, path, code, message }`. Codes include `manifest_missing`, `file_not_allowed`, `quota_exceeded`, `prefix_mismatch` and `remote_reference`. A missing screenshot is only a warning. An upload raises the content version and audits `theme.package.uploaded`. It does not switch the brand to the theme. **Upload from CI with a developer token** An admin can mint a developer token for the `atlas` CLI or a CI job. It carries one scope, `theme:write`, and reaches nothing but the two theme routes below. It needs no session and no `X-Brand-Id`, because the token names its brand. ```bash curl -s -X POST https://api.atlascms.id/v1/developer/themes \ -H "Authorization: Bearer $ATLAS_DEV_TOKEN" \ -F "package=@acme-1.0.0.zip" -F "dry_run=1" ``` POST The answer has the same shape as the dashboard upload, throttled to 10 a minute per token. `GET /v1/developer/themes` lists the brand's themes. See [Security](/docs/security) for how tokens are scoped and revoked. ### Switch the active theme Switching is an **admin** action, in two steps. POST Send `{ "theme": "acme" }`. The check writes nothing and answers `ready`, plus what the brand would lose: menu keys the theme has no location for, entries whose custom template the theme does not declare, and warnings about menu depth, the style variation and stored options the new schema would refuse. Overrides never block a switch: they stay keyed by the theme that made them and return when you switch back. PUT Send `{ "theme": "acme" }`, or `{ "theme": null }` for no theme. The API pins the synced version, clears the style variation when the slug changes, audits `theme.activated` and queues the purge webhook with `paths: ["*"]`. A theme uploaded by one brand can only be activated by that brand. ### Preview a theme before it goes live PUT Send `{ "theme": "acme" }` (admin) to set a preview draft, or `{ "theme": null }` to clear it. Only a request with a preview token renders that theme, so visitors keep seeing the live one. A preview draft writes no purge and moves no content version. ### Order of work Upload with `dry_run` until it is clean, upload for real, set the preview draft and walk the site with a preview token, then run the preflight and activate. ### Publish means content A theme package is uploaded, never published. Publish is for entries, template and part drafts and theme option drafts. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Media library Source: https://atlascms.id/docs/media On this page ## Media library How files get into a brand, what is checked on the way, and where they are stored. - 6 min read ### How media works Every file belongs to one brand. You upload it once, and pages, menus, settings and theme options refer to it by its id. The API resolves that id to URLs, size and alt text when it builds an answer, so a block never stores a link to a file. All media calls need a signed in session and the `X-Brand-Id` header. Roles: | Action | Role | | --- | --- | | List, upload, edit alt text, move files, create folders | Any member (editor and up) | | Delete a file, replace a file, delete a folder | Publisher or admin | | Storage provider and assets base URL | Admin | ### The upload flow Files do not pass through the API. The browser sends them straight to storage with a signed address, then tells the API it is done. The API then verifies what arrived. **Step 1: start the upload** POST ```json { "filename": "hero.png", "mime": "image/png", "size": 482113, "hash": "3f5a...sha256 hex of the file...", "folder_id": null } ``` `hash` is the SHA-256 of the file in hex (`shasum -a 256 hero.png`). `size` is capped per type, and a bigger file is a 422: | Type | Largest file | | --- | --- | | `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `image/gif` | 25 MB | | `image/svg+xml` | 2 MB | | `application/pdf` | 50 MB | Any other `mime` is refused. If the brand already has a ready file with the same hash, the answer returns that asset with `duplicate: true` and `upload: null`, and you skip the next steps. Otherwise the answer holds the pending asset and an `upload` object: `url`, `method` (`PUT`), `headers` and `expires_at`. **Step 2: send the bytes** `PUT` the file to `upload.url` and send exactly `upload.headers`. The declared length and checksum are signed into the address, so storage refuses any other bytes. The address points at a staging key that works once. With the `local` storage provider the address is a signed route on the API instead. ```bash curl -X PUT "$UPLOAD_URL" -H "Content-Length: 482113" \ -H "x-amz-checksum-sha256: $CHECKSUM" --data-binary @hero.png ``` Use the headers the API returned, not the ones in this example. Your bucket needs CORS for the dashboard origin when the browser does the upload. **Step 3: complete** POST Send `{ "hash": "..." }`, the same hash as in step 1. The API checks the size and checksum of the stored object, reads the real `width` and `height`, writes a 480 px thumbnail and marks the asset ready. A wrong size or checksum is a 422 and the file is dropped. A second call is a 422 too, and the old upload address can no longer change the stored file. An identical file already in the brand returns the existing asset with `deduplicated: true`. ### SVG files are cleaned An SVG can carry script, so it is never served as uploaded. At step 3 the API reads the staged bytes, removes scripts, `foreignObject`, animation elements, event handler attributes and any reference that is not a local `#fragment`, and stores only the cleaned file. A file that is not valid XML, has a doctype or entity, or is not rooted at `` is a 422 and nothing is stored. ### Folders and the list `GET /v1/manage/media` takes `folder_id` (or `none` for files without a folder), `q` (file name), `ids` (up to 40 comma separated ids), `sort` (`created_at`, `filename`, `size`), `direction` and `per_page` (1 to 100, default 40). Each row carries `usage_count`. Folders are a resource at `/v1/manage/media-folders`. A folder has a `name` (up to 120 characters) and an optional `parent_id`. Deleting a folder that still holds files or folders returns 409 until you pass `move_contents=1`, which moves everything inside up one level. Move files with `PATCH /v1/manage/media/{asset}` and a `folder_id`, or in bulk: POST ```json { "action": "move_to_folder", "folder_id": "01m...", "ids": ["01m...", "01n..."] } ``` `action` is `move_to_folder` (any member) or `delete` (publisher or admin), with 1 to 100 ids. The answer is always 200 with `{ action, done, failed, results[] }`. Each result is `moved`, `deleted`, `not_found` or `in_use`. A file that is in use is never deleted. ### Alt text and focal point PATCH ```json { "alt": { "en": "A technician replacing a phone screen", "id": "Teknisi mengganti layar ponsel" }, "focal_x": 0.4, "focal_y": 0.3 } ``` Alt text is stored per language, up to 500 characters each, and every key must be a locale of the brand. `focal_x` and `focal_y` are between 0 and 1. You can also rename the file with `filename`. A media prop in a block needs alt text, unless the prop sets `decorative: true`. The publish warnings and the dashboard health card flag images without alt text. When an edit changes `alt` or the focal point of a file that is in use, the site is purged. ### Replace a file POST A publisher sends `{ "mime", "size", "hash" }` and gets a new `upload` object, then repeats steps 2 and 3. The file keeps its id, alt text and every reference. The kind must match: an image can only replace an image, a PDF only a PDF. The API deletes the old object and thumbnail and queues a `media.replaced` purge webhook, so caches refresh. ### A file in use cannot be deleted GET The answer lists where the file is used: `entries`, `fragments`, `menus`, `settings`, `templates` and `theme`, with a `total`. `DELETE /v1/manage/media/{asset}` (publisher) answers 422 while the file is in the published or latest revision of an entry, a part, a menu, a setting such as the logo or the default share image, or the theme. The answer names the entries and `other_usages`. Remove the references first, then delete. Publishing or rolling back a revision that points at a deleted file is refused too. ### Where files are stored By default files go to the shared AtlasCMS storage. An admin can give a brand its own bucket. GET PUT ```json { "provider": "r2", "endpoint": "https://.r2.cloudflarestorage.com", "region": "auto", "bucket": "acme-media", "public_url": "https://media.acme.example", "access_key_id": "...", "secret_access_key": "..." } ``` `provider` is `shared`, `s3`, `r2`, `b2`, `custom` or `local`. A remote provider needs `bucket` and `region`, and `r2`, `b2` and `custom` also need `endpoint`. Endpoints and `public_url` must be HTTPS on port 443 and resolve to a public address. Credentials are write only: the API stores them encrypted and `GET` only reports `credentials_set`. Changing the provider, endpoint or bucket needs fresh credentials. `POST /v1/manage/media/storage/test` checks the connection without saving. Files already uploaded stay reachable after you switch. ### Switching does not move files New uploads go to the new storage. Existing files keep their keys and stay where they are. **Assets base URL** The `media` setting decides which address public files are served from and how new uploads are named. An admin writes it with `PUT /v1/manage/settings`: ```json { "settings": { "media": { "assets_base_url": "https://cdn.acme.example/atlas", "upload_structure": "year_month", "keep_filename": false } } } ``` `assets_base_url` is `null` (use the disk address) or an HTTPS address on port 443 with a normal host name. `upload_structure` is `year_month`, `year`, `flat` or a custom folder pattern. `keep_filename: true` keeps the cleaned original name instead of a random id. The database stores keys only, so changing the base URL moves every public link at once. Links pasted into rich text are not rewritten. `POST /v1/manage/media/assets-base-url/test` (admin) writes a small test file and checks that the address serves it. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Translations Source: https://atlascms.id/docs/translations On this page ## Translations One page per language, linked together, with a clear fallback when a language is missing. - 4 min read ### Languages of a brand A brand serves a list of locales, up to 20, and one of them is the default. A locale looks like `en` or `pt-BR`. The default locale is served without a prefix (`/pricing`) and every other locale is prefixed (`/en/pricing`). Platform admins set the list when they create the brand. A translation is an ordinary entry in another language. Paths are per language, so `/tentang` and `/en/about` can be the same page. ### Translation groups Entries that are the same content in different languages share a `translation_group_id`. A new entry starts its own group. Creating a translation adds an entry to the group of the entry you translate from. POST Any editor can do this. The body is: ```json { "locale": "en", "path": "/about", "title": "About us" } ``` - `locale` is required, must be a locale of the brand and must not exist in the group yet. - `path` is required for a page. For a post it is optional, and the server builds one from the permalink structure. - `title` is optional and defaults to the source title. The API copies the newest revision of the source into a new **draft** entry and remembers which revision it came from (`source_revision_id`). The answer is 201 with the new entry. Nothing goes live until a publisher publishes it. GET The answer has one row per brand locale, existing or not: ```json { "data": [ { "locale": "id", "entry_id": "01m...", "path": "/tentang", "status": "published", "outdated": false }, { "locale": "en", "entry_id": "01n...", "path": "/about", "status": "draft", "outdated": true } ] } ``` A locale with no entry has `entry_id`, `path` and `status` set to `null`. To list content by group, use `GET /v1/manage/entries?group=translations`. You get one item per group, and `missing_locale=en` narrows it to groups that have no English entry yet. Each item carries a `translations` array. ### Outdated translations A translation is **outdated** when the default language entry has a newer revision than the one the translation was made from. The default language entry itself is never outdated. The flag shows in the translation list and in the dashboard health report, so a team can see which pages need another look. When you publish a group in bulk with `include_translations: true`, every member needs a reviewed `latest_revision_id` in `revisions`. A member that changed since it was reviewed is skipped instead of published. ### What a visitor gets Public delivery never mixes languages by accident. GET If no live entry exists in the requested locale, the API serves the default language entry and says so: ```json { "data": { "locale": "id", "path": "/tentang", "fallback": true } } ``` Your site decides whether to show it, redirect, or add a banner. The fallback applies to template overrides as well. A brand can turn it off with the `locale_fallback` setting set to `false` (admin). Then a missing translation is a plain 404. **hreflang links** The page payload lists the other live languages of the same page, which is what a site needs for `hreflang`: ```json { "data": { "translations": [ { "locale": "id", "path": "/tentang" } ] } } ``` The list leaves out the current entry and any language that is not published. The front page has the path `/` in every language. `GET /v1/public/sitemap-urls` carries the same alternates for the sitemap. ### UI strings Short texts that belong to the site and not to a page, such as button labels and error messages, live in a separate store. Each string has a `namespace`, a `key` and a `locale`. GET Any member can read. Filter with `namespace` and `locale`. The list is paginated by 100. PUT A publisher saves strings in a batch of 1 to 500: ```json { "translations": [ { "namespace": "common", "key": "readMore", "locale": "id", "value": "Selengkapnya" }, { "namespace": "common", "key": "readMore", "locale": "en", "value": "Read more" } ] } ``` A value is up to 5000 characters, `namespace` up to 64 and `key` up to 255, and the locale must belong to the brand. A save audits the keys (not the values) and queues the purge webhook as `strings.updated`. `DELETE /v1/manage/translations/{translation}` removes one string. Your site reads them from the public API: GET ```bash curl -s "https://api.atlascms.id/v1/public/strings?locale=en&namespace=common" \ -H "Authorization: Bearer $ATLAS_TOKEN" ``` ```json { "data": { "locale": "en", "strings": { "common": { "readMore": "Read more" } } } } ``` Keys missing in the requested locale are filled from the default locale. Leave `namespace` out to get every namespace. ### AI translation is a draft POST Send `{ "locale": "en", "path": "/about" }` (the path is optional). The assistant writes the translation as a draft revision, in the same way a person would. A manual AI translation is never published. A member who can edit the entry reviews the draft, fixes it, and a publisher publishes it through the normal route. See [Core concepts](/docs/concepts) for how drafts and publishing differ. ### Link existing content If you imported pages in several languages, link them into groups by path (pages) or slug (posts) from the server: ```bash php artisan atlas:link-translations acme --type=page,post php artisan atlas:link-translations acme --type=page,post --write ``` The first command is a dry run. It reports how many entries it would link, how many are already linked, ambiguous or in conflict. Add `--write` to apply. ### Keep the default language as the source Translate from the default language entry. Then the outdated flag tells you exactly which translations to refresh after an edit. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Menus and fragments Source: https://atlascms.id/docs/menus-fragments On this page ## Menus and fragments Navigation and the pieces every page shares, saved per language. - 4 min read ### What they are A **menu** is a tree of links, such as the main navigation or the footer links. A **fragment** is a block tree that many pages share, such as the header, the footer or a top banner. Both are saved per language, so `header` in `id` and `header` in `en` are two records. Both are site wide. A change is not tied to one page and not to one revision, so it does not have a draft step. Saving a menu or a fragment changes live output at once. That is why writing needs a **publisher** (or an admin). Anyone who belongs to the brand can read them. ### Menus GET Lists every menu of the brand, ordered by key and language. PUT Replaces the whole tree of one menu, or creates it. `locale` must be a language of the brand, else the answer is 404. The `key` is a free name, and a theme can declare the keys it renders in `menuLocations`. ```json { "items": [ { "label": "Products", "url": "/products", "eyebrow": "Catalogue", "key": "products", "children": [ { "label": "Spare parts", "description": "Screens, batteries and chargers", "children": [ { "label": "Screens", "url": "/products/screens" } ] } ] }, { "label": "Contact", "url": "/contact" } ] } ``` **Item fields** Every item, at every depth, accepts these fields. Any other key is dropped. | Field | Rule | | --- | --- | | `label` | Required, plain text, up to 255 characters. | | `url` | Optional, up to 2048 characters, must be a safe link. | | `children` | Optional list of items, up to 100. | | `eyebrow` | Plain text, up to 120. | | `description` | Plain text, up to 500. | | `media_id` | Id of a ready media file of this brand. Another brand's id or a missing file is a 422. | | `attributes` | Up to 10 pairs of `label` (60) and `value` (120), plain text. | | `key` | A stable id for styling or tracking: `a-z`, `0-9` and `-`, up to 64. | Plain text means no HTML. A tree holds 500 items at most, children included. **Depth and unsafe links** A menu nests three levels deep at most (top item, tab, tier). If the active theme declares a smaller `maxDepth` for that menu key in `menuLocations`, the smaller number applies. A deeper tree is a 422 on `items`. A link must be a web address, an email or phone link, or a path on the site. Anything else, such as a `javascript:` address, is refused and the error names the exact item so the editor can show it on the field: ```json { "errors": { "items.0.children.1.url": ["Must be a web address, an email or phone link, or a path on this site."] } } ``` **Reading a menu on your site** GET ```bash curl -s "https://api.atlascms.id/v1/public/menus/header?locale=en" \ -H "Authorization: Bearer $ATLAS_TOKEN" ``` If the menu does not exist in the requested language, the API returns the default language one. If neither exists, it answers 404. An item with a `media_id` comes back with `image: { url, alt, width, height }`, with alt text in the menu language and then the default language. If the file is gone, `image` is `null`. `DELETE /v1/manage/menus/{menu}` removes a menu (publisher). A media file used by a menu cannot be deleted until the menu stops using it. ### Fragments GET PUT ```json { "blocks": [ { "id": "h1", "type": "acme-header", "v": 1, "props": {} } ], "published": true } ``` Common keys are `header`, `footer` and `banner`. `blocks` is a block tree checked like any other (three levels, safe links, props against the block definitions). A fragment never holds the `atlas-post-content` block, because it appears on many pages and has no single entry body. `published: false` saves the blocks without making them live. A part with no live override falls back to the default the theme package ships. Your site does not call a fragments endpoint. The page payload from `GET /v1/public/page` carries `fragments`, keyed by area (`header`, `footer`, `banner`). If a language has no fragment, the default language fills the gap. `DELETE /v1/manage/fragments/{fragment}` removes one (publisher). **Fragments are template parts** A fragment is an alias of a **template part** of the active theme. The endpoints above write the same override a part has, and keep the old fragment record as a mirror, so a site that reads `fragments` keeps working. The alias stays for two minor versions. New work should use the part endpoints, which also give you a draft before publishing: | Call | Role | | --- | --- | | `PUT /v1/manage/theme/templates/part/{slug}/draft` | Publisher, saves `{ locale?, blocks }` as a draft | | `POST /v1/manage/theme/templates/part/{slug}/publish` | Publisher, makes the draft live | The theme side of this is explained in [Theme development](/docs/theme-development). ### How a change reaches the live site Every save of a menu or a fragment runs the same steps: - The change is saved and an audit row is written (`menu.updated`, `menu.deleted`, `fragment.updated`, `fragment.deleted`). - The brand content version goes up. The public API builds its ETags from that version, so clients and caches see a new answer at once. - A signed purge webhook is queued to each active webhook of the brand. Its `paths` is `["*"]`, because a menu or a fragment can appear on every page. Your site checks the signature, drops its cached pages, and the next visitor gets the new header. See [Webhooks and purge](/docs/webhooks) for the headers and a verification example. ### No draft for menus A menu has no draft or schedule. If you need to prepare a new navigation first, build it under another key, then switch your site to read that key. ### A save replaces the tree `PUT` sends the whole menu. Read it with `GET /v1/manage/menus` first, change the items you need, then send the full tree back. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Forms and leads Source: https://atlascms.id/docs/forms-leads On this page ## Forms and leads From a contact form on your site to a lead your sales team can follow up. - 5 min read ### How it works A form is a small definition an admin saves for the brand: a key, a list of fields and a success message. Your site posts visitor input to the public endpoint. ATLAS validates it against the definition, stores it as a lead, queues a forward to your CRM, notifies your team and, when the visitor consented, tells the ad platforms about the conversion. Forms depend on the `forms` module. When a brand switches that module off, the public endpoint answers 404 and the manage calls for forms and leads answer 403. ### Define a form Forms are managed by an **admin**. All calls are under `https://api.atlascms.id/v1/manage` and need a signed in session plus the `X-Brand-Id` header. GET Lists the forms of the brand, ordered by key. `POST /v1/manage/forms` creates one (201), `PUT /v1/manage/forms/{form}` updates it and `DELETE /v1/manage/forms/{form}` removes it (204). Every change is written to the audit log. | Field | Rule | | --- | --- | | `key` | Required on create, cannot change later. Lowercase letters, digits, `_` and `-`, starting with a letter, up to 48 characters, unique in the brand. | | `name` | Required, up to 120 characters. | | `fields` | Required, 1 to 30 fields. | | `success_message` | Optional, up to 255 characters. Sent back to the visitor. | | `active` | Optional boolean. An inactive form answers 404 to visitors. | Each field in `fields` has these properties: | Property | Rule | | --- | --- | | `name` | Required. Lowercase letters, digits and `_`, starting with a letter, up to 32 characters, unique in the form. | | `type` | Required: `text`, `email`, `phone`, `textarea` or `select`. | | `required` | Optional boolean. | | `max` | Optional, 1 to 5000. The length limit. Text fields are capped at 1000 and textareas at 5000 whatever you set. | | `options` | For `select`: up to 50 allowed values, each up to 120 characters. | ```json { "key": "contact", "name": "Contact form", "fields": [ { "name": "name", "type": "text", "required": true }, { "name": "email", "type": "email", "required": true }, { "name": "phone", "type": "phone" }, { "name": "topic", "type": "select", "options": ["Sales", "Support", "Partnership"] }, { "name": "message", "type": "textarea", "max": 2000 } ], "success_message": "Thanks, we will reply within one working day.", "active": true } ``` ### Name fields `email` and `phone` The tracking events below read the fields named `email` and `phone`. Use those names if you want conversions matched on the ad platforms. ### Submit from your site POST Send the visitor's input as JSON. Only fields the form declares are accepted and stored. The brand comes from your read token (`Authorization: Bearer atl_read_...`) or from the `Host` header. | Field | Description | | --- | --- | | `fields` | Required object keyed by field name. | | `consent` | Optional boolean. Stored with the lead and decides whether a tracking event is sent. | | `utm` | Optional object with `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` and `utm_id`, each up to 255 characters. | | `fbclid`, `gclid` | Optional click ids, up to 255 characters. | | `source_page`, `referrer` | Optional, up to 2048 characters. | | `locale` | Optional, up to 8 characters. | | `website` | Honeypot. Keep it in the page as a hidden input that people never fill. | ```bash curl -s -X POST "https://api.atlascms.id/v1/public/forms/contact" \ -H "Authorization: Bearer $ATLAS_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Atlas-Client-Ip: 203.0.113.7" \ -d '{ "fields": { "name": "Sari", "email": "sari@example.com", "message": "Hello" }, "consent": true, "utm": { "utm_source": "newsletter" }, "source_page": "/contact", "website": "" }' ``` ```json { "data": { "id": "01m518vvbf8txh40wcvwhvtzyd", "message": "Thanks, we will reply within one working day." } } ``` | Status | Meaning | | --- | --- | | 201 | Stored. The body holds the lead `id` and your `success_message`. | | 202 | The honeypot `website` field was filled. The visitor sees the same message, but nothing is stored. | | 404 | Unknown or inactive form, or the `forms` module is off. | | 422 | A field failed validation. Body: `{ "message", "errors": { "field": ["..."] } }`. | | 429 | More than 10 submissions a minute for the same token and visitor address. | Validation follows the field type: `email` must be a valid address (up to 254 characters), `phone` accepts an optional `+`, digits, spaces, brackets and hyphens (6 to 20 characters), and `select` must be one of the declared options. **Pass the visitor, not your server** When your site's server posts the form, send the read token plus two headers so the visitor is judged and stored, not your server. | Header | Value | | --- | --- | | `X-Atlas-Client-Ip` | The visitor's address. It is used for the rate limit and stored only as a salted hash. | | `X-Atlas-Client-User-Agent` | The visitor's user agent, kept up to 255 characters. | ATLAS believes these headers only on a call that carries a read token. Without one, the connection address is used. The raw IP address is never stored. ### Work the lead inbox Leads hold personal data. Every call in this section needs a **publisher** or **admin**. An editor gets 403, even to read. A lead has two statuses. `status` is where it stands with your team: `new` (default), `contacted`, `qualified`, `spam` or `archived`. `delivery_status` is the CRM forward: `pending`, `forwarded`, `failed` or `skipped`. GET Filters: `form` (the form key), `status`, `delivery`, `assigned` (a member id, `me` or `none`), `from` and `to` (`Y-m-d` days in the brand timezone), `q` (searches the submitted fields, the note and the source page), `sort` (`created_at` or `status`, prefix `-` to reverse) and `per_page` (1 to 100, default 50). `meta.status_counts` gives the count per status for the current filters. | Call | What it does | | --- | --- | | `GET /v1/manage/form-submissions/{id}` | The lead, its assigned member and up to 50 activities (`received`, `status_changed`, `note_updated`, `assigned`, `delivery`). Activities never contain submitted values. | | `PATCH /v1/manage/form-submissions/{id}` | Change `status`, `note` (up to 2000 characters, `null` clears it) or `assigned_to` (a member of the brand, or `null`). Nothing else can change. | | `POST /v1/manage/form-submissions/bulk` | `{ "action": "status", "status": "contacted", "ids": [...] }` or `{ "action": "delete", "ids": [...] }`, 1 to 200 ids. Answers `{ action, affected, skipped }`. | | `DELETE /v1/manage/form-submissions/{id}` | Permanently deletes the personal data (204). | | `POST /v1/manage/form-submissions/{id}/retry` | Queues the CRM forward again. | | `GET /v1/manage/leads/summary` | Totals for a window (default last 30 days), split by form, status and day. | | `GET /v1/manage/leads/assignees` | Members a lead can be assigned to: `id`, `name` and `avatar_url` only. | ```bash curl -s -X PATCH "https://api.atlascms.id/v1/manage/form-submissions/01m518vvbf8txh40wcvwhvtzyd" \ -H "X-Brand-Id: $BRAND_ID" -H "Content-Type: application/json" \ --cookie "$SESSION" \ -d '{ "status": "contacted", "note": "Called on Monday", "assigned_to": null }' ``` Spam is counted separately: the summary and the dashboard count every submission that is not `spam`. **Export to CSV** GET Takes the same filters as the list and streams a `text/csv` download (UTF-8 with BOM, 10 requests a minute). Columns are id, created_at, form, status, delivery_status, locale, consent, source_page, referrer, the `utm_*` values, fbclid, gclid, note, and one `field_` column per form field. The export is written to the audit log with its filters. A cell that starts with `=`, `+`, `-`, `@`, a tab or a carriage return gets a leading `'`, so a spreadsheet never runs a submitted value as a formula. ### Forward leads to a CRM Right after a lead is stored, ATLAS queues a forward to the brand's `crm` integration. Without a configured endpoint the lead stays in ATLAS with `delivery_status: skipped`. A failed delivery is tried 5 times in total, waiting 10 seconds, 1 minute, 5 minutes and 15 minutes between tries, then marked `failed` with a short reason. Use **retry** to send it again. Configure it with `PUT /v1/manage/integrations/crm` (admin): | Config key | Description | | --- | --- | | `endpoint` | Public HTTPS address on port 443. Private addresses are refused. | | `auth_header` | Header that carries the token: `Authorization` (default), `X-Website-Api-Key` or `X-Api-Key`. | | `auth_scheme` | `Bearer` (default) sends `Bearer `. `none` sends the raw token. | | `payload` | `atlas` (default) sends the full lead. `flat` puts the form fields at the top level, flattens `utm_source`, `utm_medium` and `utm_campaign`, and adds `atlas_lead_id` and `source_page`. | ```json { "enabled": true, "config": { "endpoint": "https://crm.example.com/api/leads", "auth_header": "X-Api-Key", "auth_scheme": "none", "payload": "atlas" } } ``` The token is a write only secret. Save it with `PUT /v1/manage/secrets/crm/token` and body `{ "value": "..." }`. It is encrypted, never returned and never written to logs or audit rows. Saving a different `endpoint` deletes the stored token, so enter it again. The default `atlas` payload looks like this. Each request also carries `X-Atlas-Delivery` with the lead id, so your CRM can ignore a repeat. ```json { "id": "01m518vvbf8txh40wcvwhvtzyd", "brand": "atlascms", "form": "contact", "locale": "en", "fields": { "name": "Sari", "email": "sari@example.com", "message": "Hello" }, "utm": { "utm_source": "newsletter" }, "fbclid": null, "gclid": null, "source_page": "/contact", "referrer": null, "consent": true, "submitted_at": "2026-10-21T14:42:20+00:00" } ``` ### Conversion tracking When a visitor ticks consent (`"consent": true`), ATLAS also sends a `Lead` conversion event from the server to every enabled ad platform integration (`meta_pixel` with its Conversions API token, `tiktok_pixel` with its access token). The lead id is the event id, so a matching browser pixel event is deduplicated. Email and phone are hashed before they leave ATLAS. A lead without consent sends no event. Send a test event with `POST /v1/manage/integrations/{provider}/test` (admin, 10 a minute). An optional `test_event_code` keeps it out of real reports. ### Notifications When a lead arrives, every publisher and admin of the brand gets an in app notification. The first 5 leads in a 10 minute window notify one by one. After that, one roll up notification with a count is sent per window, so a burst never floods the team. ### Privacy rules - Only publishers and admins can read, change, export or delete leads. A lead of another brand is a 404. - Visitor IP addresses are stored only as a salted hash. - Secrets, tokens and submitted values never appear in audit rows, logs or URLs. The audit log records that a lead changed, never the note text. - Brand backups leave lead fields out. See [Backups](/docs/backups). - Deleting a lead is permanent. Delete on request, and keep your own privacy notice in step with what you collect. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # AI drafting and autopost Source: https://atlascms.id/docs/ai On this page ## AI drafting and autopost What the AI actions do, how they are limited, and the one place where AI output can go live. - 5 min read ### Draft only Every AI action saves its result as a new **draft revision** of an entry, marked with the source `ai`. Nothing is published, scheduled or approved by an AI call. A person opens the draft, reads the diff and decides. The model has no tools of its own. It cannot call your API, browse, or write on its own. ATLAS validates what it returns, and rejects an answer that has the wrong shape, uses blocks or media the entry does not have, contains an unsafe link or uses a word on your forbidden list. A rejected answer saves nothing. There is one exception, which an admin has to switch on: autopost with auto publish (see Autopost below). Every manual action stays draft only. ### Actions on an entry These calls need a signed in dashboard session and the `X-Brand-Id` header. Any member who may edit the entry can run them, and a content type with a higher `min_role` still blocks lower roles. POST Writes or rewrites a page. The body has a `brief` (3 to 4000 characters) and a `mode`: `draft`, `rewrite` or `continue`. ```json { "brief": "A short intro to our repair service for small shops.", "mode": "draft" } ``` The answer is 201 with `data.run` (the AI run), `data.revision` (the new draft) and `data.diff` (a word diff against the previous version). POST The same body and permissions, answered as server sent events. You get `event: delta` chunks with `{text}` (the raw JSON the model produces), then `event: done` with `{data: {run, revision, diff}}` once the draft is saved, or `event: error` with `{status, message}`. Read it with `fetch` and a stream reader: `EventSource` cannot send a POST. POST Creates a translation of the entry as a draft. The body has the target `locale` (one of the brand's other languages) and an optional `path`. The answer also carries the new `entry`. POST Suggests an SEO title and description as a draft revision. No body. POST Looks at the entry's images and returns alt text suggestions on the AI run (201, `data.run`). Nothing reaches the media library yet. POST An editor accepts the suggestions of a completed alt text run, and only then is the alt text stored on the media assets. It needs permission to create media. ### Brand voice and quota GET Any member can read the AI profile. An admin saves it with `PUT /v1/manage/ai/profile`. The writer uses it on every call. | Field | Meaning | | --- | --- | | `tone` | How the brand sounds, up to 255 characters. | | `audience` | Who you write for, up to 1000 characters. | | `forbidden_words` | Up to 100 words the AI must not use. | | `glossary` | Up to 100 terms to keep consistent. | | `sample_posts` | Up to 5 samples of your style, 6000 characters each. | | `monthly_token_quota` | Tokens this brand may use per month. It is capped by a ceiling the installation sets. | GET Returns this month's `used_tokens`, `quota`, `remaining`, `cost` and `runs`. Before each call ATLAS holds an estimate against the quota and settles it afterwards, so two calls at once cannot overspend it. When nothing is left, the call is refused with 429. `GET /v1/manage/ai/runs` lists AI runs (admins only, 50 a page, filter by `feature` and `status`). Each run records the feature, model, tokens, cost, status and diff. ### What the errors mean | Status | Meaning | What to do | | --- | --- | --- | | 503 | AI is not set up on this installation. | Ask the operator to configure a provider. | | 502 | The provider did not answer, refused, or timed out. Nothing was changed. | Try again in a moment. | | 429 | The monthly quota is used. | Wait for next month or ask an admin to raise the quota. | | 422 | The answer failed validation and was not saved. | Change the brief and try again. | The streaming endpoint sends the same codes in its `event: error`. ### Providers ATLAS asks Gemini first. If that provider fails or has no key, it tries OpenRouter next. A provider without a key is skipped. You do not choose the model per request, and you cannot bring your own key in the dashboard: the operator of the installation configures the providers. Keys are never returned or logged. ### Autopost Autopost writes new posts for you on a schedule. An admin turns it on per brand. GET Any member can read the settings. An admin saves them with `PUT /v1/manage/ai/autopost/settings`. ```json { "enabled": true, "auto_publish": false, "content_type_key": "post", "term_id": null, "locale": "en", "posts_per_day": 2, "hours": ["09:00", "15:00"], "topic_source": "both", "topics": ["How to choose a laptop for your shop"], "trends_geo": "ID", "trends_keywords": ["laptop"], "cover_image": true, "author_id": 12 } ``` - `content_type_key` can only be `post` today. - `posts_per_day` is 1 to 24. `hours` are 1 to 24 slots as `HH:MM` in the brand's timezone. - `topic_source` is `manual`, `trends` or `both`. A manual or mixed list needs at least one topic (up to 500). With `trends`, ATLAS draws on stored trending topics for `trends_geo`, optionally filtered by `trends_keywords` (up to 20). - `cover_image` paints one cover image and sets it as the post's social image. If the image fails, the article is still saved. - `author_id` must be an active member of the brand. Each due slot writes one new post. An article is planned and then written section by section, so a run takes minutes and costs tens of thousands of tokens from your quota. A post that is too short is refused. A failed slot is retried after ten minutes. By default the post is saved as a draft and a **review request** is opened for your publishers. They approve it like any other review. `GET /v1/manage/ai/autopost/runs` lists recent slots with `topic`, `status` (`pending`, `success`, `failed`), a short `reason` and the `entry_id`. POST Admins can queue one slot right now (10 a minute). It answers 202 with `{"data": {"queued": true}}`. Autopost switched off is a 422. The installation also runs the command `atlas:ai-autopost` every minute, which picks up due slots for every enabled brand. **Auto publish** ### The one exception `auto_publish` is off by default and only an admin can turn it on. When it is on, autopost publishes the post it just wrote through the normal publish path, as a system actor with the author you chose, and writes an audit row (`autopost.published`). The review step is skipped. Leave it off unless you are happy to put unreviewed AI text on your site. Changes to the autopost settings are audited as `autopost.settings_updated`. ### Backfill POST Fills missing SEO titles, descriptions and image alt text on your **published** entries, as drafts only. It is admin only (3 a minute) and needs the brand setting `ai_backfill` switched on, otherwise it answers 409 `backfill_disabled`. `limit` is optional (1 to 50, default 10). The answer is `{data: {entries, totals, stopped, next_cursor}}`. A run stops when the quota is nearly used, at the limit, after about 25 seconds, or after three provider failures. Pass `next_cursor` back as `after` to continue. An entry that already has a newer draft is skipped. Alt text stays a suggestion until an editor accepts it. For roles and reviews, see [roles and permissions](/docs/roles). For agents that call ATLAS from outside, see [MCP and agent access](/docs/mcp-agents). ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # MCP and agent access Source: https://atlascms.id/docs/mcp-agents On this page ## MCP and agent access How to give an agent safe access to a brand, and what it can and cannot do. - 5 min read ### What an agent token is An agent token is a credential for an outside program, such as an AI assistant, that works on one brand. It starts with `atl_agent_`, ATLAS shows it **once** when it is created and keeps only a hash. Behind each token sits a hidden agent user with a fixed **editor** role. That user never appears in the member list. Because the role is editor, the same rules that apply to a person apply to the agent: content types with a higher `min_role` are invisible to it, and a record from another brand is a 404. An agent token carries scopes. You pick at least one when you create it. | Scope | What it allows on the agent door today | | --- | --- | | `read` | Read the brand, content types, block schemas, entries (drafts included), revisions, terms, menus and a small dashboard. | | `draft:write` | Create a draft entry, save a draft, and set the terms of a draft entry. | | `media:write`, `translate`, `review:request`, `settings:read` | Can be minted, but no route on the agent door uses them yet. | The `publish` and `schedule` scopes cannot be minted. Asking for them answers 422. ### Agents never publish Everything an agent writes is a draft revision. A person opens it in the dashboard, reviews it and publishes it. There is no publish, schedule, rollback or delete route on the agent door and no such tool on the MCP endpoint. ### Create an agent token An admin turns agent access on for the brand first. The brand setting `agent.enabled` is off by default. While it is off, creating an agent token answers 403 `agent_disabled`. POST This is a dashboard call, so it needs a signed in admin session and the `X-Brand-Id` header. ```json { "name": "Content assistant", "kind": "agent", "scopes": ["read", "draft:write"], "expires_in_minutes": 43200 } ``` `expires_in_minutes` is optional. It must be between 15 minutes and 129600 minutes (90 days), and it defaults to 43200 (30 days). The answer is 201 with the token row and the plain `token` value. Store it in a secret manager right away. Revoking the token (`DELETE /v1/manage/api-tokens/{token}`) disables the hidden agent user at once. Expired agent tokens are disabled by an hourly sweep as well. ### The agent door The agent door lives under `https://api.atlascms.id/v1/agent`. The token names its brand, so you send no `X-Brand-Id` and no cookie. ```bash curl -s "https://api.atlascms.id/v1/agent/entries?per_page=10" \ -H "Authorization: Bearer $ATLAS_AGENT_TOKEN" ``` | Method | Path | Scope | | --- | --- | --- | | GET | `/v1/agent/brand` | `read` | | GET | `/v1/agent/content-types`, `/v1/agent/content-types/{key}` | `read` | | GET | `/v1/agent/block-schemas` | `read` | | GET | `/v1/agent/entries`, `/v1/agent/entries/{entry}`, `/v1/agent/entries/{entry}/revisions` | `read` | | GET | `/v1/agent/terms`, `/v1/agent/menus`, `/v1/agent/dashboard` | `read` | | POST | `/v1/agent/entries` | `draft:write` | | PUT | `/v1/agent/entries/{entry}/draft` | `draft:write` | | PUT | `/v1/agent/entries/{entry}/terms` | `draft:write` | Lists take `per_page` (20 by default, 50 at most). Media upload, menu writes, term creation, translations and reviews are not on this door yet. **Writes need an idempotency key** Every write must carry an `Idempotency-Key` header of 16 to 64 letters, digits, dots, colons, dashes or underscores. Without it the call is 422 `idempotency_key_required`. ```bash curl -s -X POST "https://api.atlascms.id/v1/agent/entries" \ -H "Authorization: Bearer $ATLAS_AGENT_TOKEN" \ -H "Idempotency-Key: 01JD6N9V0Q8KZC3F5H7M2T4W6Y" \ -H "Content-Type: application/json" \ -d '{"type":"page","locale":"en","path":"/about","title":"About","blocks":[]}' ``` - Retrying with the same key and the same body replays the first answer (header `Idempotent-Replay: true`) and writes nothing. - The same key with a different body is 422 `idempotency_key_reuse`. A call while the first is still running is 409 `idempotency_in_progress`. - Keys are per token and are kept for 24 hours. - Saving a draft needs `base_revision_id`, the head revision the agent read. If a person saved in the meantime the answer is 409 `revision_changed` and nothing is written. - Saving a draft of a published entry never changes the live page. **Limits and quotas** | Limit | Value | | --- | --- | | Reads | 120 calls a minute per token and 600 a minute per brand | | Writes | 30 a minute per token and 150 a minute per brand | | Daily writes | 500 per token, then 429 `agent_quota_exceeded` | | Body size | 256 KiB, then 413 `payload_too_large` | Other limits answer 429 with a `Retry-After` header. ### Remote MCP endpoint [MCP](https://modelcontextprotocol.io) is a standard way for an AI client to call tools. `POST /mcp` exposes the agent door as a remote MCP server, for clients that cannot run a local package. - It is **off by default**. The installation turns it on with `ATLAS_MCP_REMOTE=true`. While it is off the path answers 404. - It speaks Streamable HTTP in stateless mode: one JSON-RPC request per `POST`, one JSON answer. `GET /mcp` is 405. - Auth is `Authorization: Bearer atl_agent_...` only. A token in the query string is refused. - A request with an `Origin` header must come from an origin listed in `ATLAS_MCP_ALLOWED_ORIGINS` (empty by default), else 403. Plain HTTP is refused in production. - The brand switch `agent.enabled` still applies. `tools/list` returns 13 tools. Ten read tools need the `read` scope: `get_brand`, `list_content_types`, `get_content_type`, `list_block_schemas`, `list_entries`, `get_entry`, `list_revisions`, `list_terms`, `list_menus` and `get_dashboard`. Three write tools need `draft:write`: `create_entry`, `save_draft` and `sync_entry_terms`. A tool call runs the matching agent route in process, so scopes, limits, quotas and audit rows are the same as on the door. A write tool takes its key in `params._meta.idempotencyKey`. ```json {"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "create_entry", "arguments": {"type": "page", "locale": "en", "path": "/about", "title": "About", "blocks": []}, "_meta": {"idempotencyKey": "01JD6N9V0Q8KZC3F5H7M2T4W6Y"} }} ``` A client that takes headers can be configured like this: ```json { "mcpServers": { "atlas": { "type": "http", "url": "https://api.atlascms.id/mcp", "headers": { "Authorization": "Bearer ${ATLAS_AGENT_TOKEN}" } } } } ``` Let the client read the token from the environment or a secret store. Never commit it. ### Safety and control - **Content is data, not instructions.** Free text in answers sits under `content`, and the answers carry `meta.trust: "untrusted_content"`. An MCP result repeats that warning. Treat page text as untrusted input in your agent. - **Token-shaped strings are redacted.** Any value that looks like an ATLAS token is replaced by `[redacted]` in answers. - **Kill switches.** Turn off `agent.enabled` to stop new agent tokens. To stop a token you already issued, revoke it. The installation also has platform switches that refuse all agent reads or all agent writes at once. - **Audit rows.** Every agent call writes an `agent.call` audit row with the tool, scope, outcome and status, never a body or title. Rows are kept for 90 days. Admins can read them with `GET /v1/manage/audit-logs?action=agent.call`. - **Least privilege.** Mint the shortest-lived token with the fewest scopes the job needs, one token per agent, and rotate it by minting a new one before revoking the old one. ### The sync token is not an agent token A separate `sync` token kind exists only for the project's own CI to publish its documentation and changelog. You cannot use it as an agent token, and you do not need it. For roles, token kinds and who may create them, see [roles and permissions](/docs/roles). For the full list of token kinds, see [tokens and security](/docs/security). ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Roles and permissions Source: https://atlascms.id/docs/roles On this page ## Roles and permissions Who may write, who may publish, and who may change the settings of a brand. - 3 min read ### Three roles per brand Every member of a brand has exactly one role in that brand. The same person can be an editor in one brand and an admin in another. The API checks the role before it looks at the request body, so a call you may not make fails with **403** whatever you send. | Role | Can | | --- | --- | | Editor | Write drafts, request a review, upload media, create terms, and move pages that are not live. | | Publisher | Everything an editor can, plus publish, schedule, roll back and delete entries, change or move live pages, manage redirects, menus, fragments and strings, delete media, retry webhook deliveries, and read and manage leads. | | Admin | Everything a publisher can, plus settings, theme, modules, block definitions, webhooks, members, API tokens, integrations, secrets, custom scripts, imports and the audit log. | A brand always keeps at least one active admin. You cannot remove or suspend the last one. ### Platform admin is different A platform admin manages brands of the whole installation (create, rename, suspend). It is not a brand role, it is set only on the server command line, and it gives no access to the content of a brand. To read or edit a brand, a platform admin still needs a membership there. ### Minimum role per content type An admin can give a custom content type a `min_role` of `editor`, `publisher` or `admin`. Without one, every member can use the type. A member who ranks below `min_role` gets 403 on that type's entries: list filters, show, create, save a draft, publish, revisions, AI actions, reviews and exports. Unfiltered lists simply leave those entries out. Public delivery is not affected. ### Review workflow An editor cannot publish, so they ask for a review. A review pins one revision, so approving it publishes exactly what was reviewed. | Step | Call | Who | | --- | --- | --- | | Request | `POST /v1/manage/entries/{entry}/review` with an optional `note` | Any member | | List | `GET /v1/manage/reviews` | Publishers and admins see all, an editor sees their own | | Approve | `POST /v1/manage/reviews/{review}/approve`, with `publish: true` to go live | Publisher, admin | | Send back | `POST /v1/manage/reviews/{review}/changes` with a required `note` | Publisher, admin | | Cancel | `POST /v1/manage/reviews/{review}/cancel` | The requester, publisher, admin | | Comment | `POST /v1/manage/reviews/{review}/comments` with a `body` | The requester, publisher, admin | If the page changed after the review was requested, approving with `publish: true` answers 409 `revision_changed` and nothing goes live. A new request for a newer version cancels the older open one. Every step is audited and sends a realtime update. ### Members and invitations Only admins manage members. All calls need the `X-Brand-Id` header. | Call | What it does | | --- | --- | | `GET /v1/manage/members` | Lists members with `role` and `status` (`active` or `suspended`). | | `POST /v1/manage/members` | Body `{email, role}`. Adds an existing account (201) or, for an unknown email, creates an invitation and answers 202. | | `PATCH /v1/manage/members/{user}` | Changes a member's role. | | `POST /v1/manage/members/{user}/suspend` | Blocks the member in this brand. Other brands are unaffected. | | `POST /v1/manage/members/{user}/unsuspend` | Restores the member. | | `DELETE /v1/manage/members/{user}` | Removes the member from the brand. | | `GET /v1/manage/invitations` | Lists pending invitations. | | `POST /v1/manage/invitations/{id}/resend` | Sends a fresh link and extends the invitation to 7 days. | | `DELETE /v1/manage/invitations/{id}` | Revokes an invitation. | ```json { "email": "maya@example.com", "role": "editor" } ``` The invitation link is sent only by email and is never part of an API answer, so whoever holds the link must own the inbox. If the brand has no working mail connection, the answer says `email_sent: false`: fix the mail settings and press Resend. A suspended member keeps the membership and role but gets 403 on every call of that brand. ### Token kinds and who creates them `POST /v1/manage/api-tokens` creates a token. Admins can create every kind. The one exception for other members is a short preview token. | Kind | Who may create it | Expiry | | --- | --- | --- | | `read` | Admin | Optional, 1 to 43200 minutes. Without one it never expires. | | `preview` | Admin, or any member | Required. Up to 7 days for an admin, 1 to 120 minutes for a member. | | `agent` | Admin, when the brand setting `agent.enabled` is on | 15 minutes to 90 days, 30 days by default. | | `developer` | Admin | 15 minutes to 90 days, 30 days by default. | A member who asks for any other kind, or a longer preview, gets 403. Only admins can list and revoke tokens. See [tokens and security](/docs/security) for how to send them and [MCP and agent access](/docs/mcp-agents) for agent tokens, which write drafts only and always act with the editor role. ### Other brands and the audit log - **A record of another brand is a 404, not a 403.** Every query is limited to the active brand, so a foreign id looks exactly like one that does not exist, and ids do not leak. - **A suspended brand answers 403** on public and dashboard calls alike. - **Authorization runs first.** The role and content type checks happen before validation, so a forbidden call never reveals whether the body was valid. - **Every live change is audited.** Publishing, rollbacks, deletes, setting changes, member and token changes write an audit row with ids, never content bodies or secrets. `GET /v1/manage/audit-logs` lists the audit rows to admins, newest first, 50 a page. Add `?action=member.suspended` to filter by one action. For the full list of rules, see [tokens and security](/docs/security). ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Imports and exports Source: https://atlascms.id/docs/imports-exports On this page ## Imports and exports Dry run first, check the report, then write. Every download sits behind a signed link. - 6 min read ### The rule: dry run first Every importer works in two steps. A **dry run** reads your source and produces a report: what would be created, updated or skipped, which slugs collide, which images failed and which links break. Nothing is written. When the numbers look right, you run the import for real, and it reuses the file you already uploaded. Imports are safe to repeat. Each imported item remembers where it came from, so a second run updates changed items and leaves the rest alone. ### Imports never publish unsafe markup Imported HTML goes through the same sanitiser and block converter as everything else. Scripts, event attributes and `javascript:` links never reach your content, and every imported link passes the link guard. All calls below are under `https://api.atlascms.id/v1/manage/tools`. They need a signed in session and the `X-Brand-Id` header. | Task | Who | | --- | --- | | WordPress and CSV imports, redirects CSV import, exports | publisher or admin | | Website crawl import, database importers, theme bundle | admin | ### Import a WordPress export or a CSV GET Lists the sources you may use: `wordpress` (a `.xml` export), `csv`, and for admins `url`. POST Send multipart form data with `kind` (`wordpress` or `csv`), `file` and `options`. Add `?dry_run=1` for the dry run. The file can be up to **2 MB**. ```bash curl -s -X POST "https://api.atlascms.id/v1/manage/tools/imports?dry_run=1" \ -H "X-Brand-Id: $BRAND_ID" --cookie "$SESSION" \ -F kind=wordpress -F file=@export.xml \ -F 'options={"locale":"en","status":"keep","download_images":true}' ``` A dry run is answered in the same request with 201 and the report. A real run is queued and answered with 202. To write after a dry run, send `run` (the dry run id) instead of the file: ```bash curl -s -X POST "https://api.atlascms.id/v1/manage/tools/imports" \ -H "X-Brand-Id: $BRAND_ID" --cookie "$SESSION" \ -F kind=wordpress -F run=01m5200000000000000000000a ``` Only one import runs per brand at a time. Poll `GET /v1/manage/tools/imports/{run}` for the report and `GET /v1/manage/tools/imports/{run}/items` for the outcome of each entry. | Option | Description | | --- | --- | | `locale` | Language of the imported content. Must be one of the brand's languages. | | `status` | `keep`, `draft` or `published`. Private posts always stay drafts, and a member who cannot publish never publishes. | | `type_map` | WordPress only: `{ "post": "post", "page": "page" }`, each `post`, `page` or `skip`. | | `type` | CSV only: `post` or `page`. | | `category_map`, `tag_map` | `{ "old-slug": "new-slug" }`. A `null` value drops the term. | | `download_images` | Copy images into the media library and redirect the old upload addresses. | | `rewrite_links` | Turn links to the old site into relative links (default on). | | `path_prefix` | CSV only, for example `/blog`. | **What WordPress imports do** - Posts and pages become entries at their WordPress permalink. Other post types, trashed posts and attachments are skipped and counted. Comments are ignored. - Authors are matched to brand members by email, otherwise the importing user. - Gutenberg comments are removed, `[caption]` and `[gallery]` become figures, other shortcodes are dropped with a warning. - With `download_images`, images are fetched over HTTPS from public addresses only (15 MB cap each), checked as real images and stored once. - Duplicate slugs keep the first entry. The later ones become drafts under a suffixed path. **CSV columns** `title, slug, date, status, body, excerpt, category, tags, locale, image_url, meta_title, meta_description` Download a ready template from `GET /v1/manage/tools/imports/csv-template`. A row is identified by type, locale and slug, so uploading the file again updates the same entries. **Limits and clean up** A WordPress file may hold up to 50,000 items, a CSV up to 20,000 rows, and one item is capped at 5 MB. Archives, binary files and XML with entity declarations are refused before parsing. The uploaded file is kept privately and removed when a real run ends, with `DELETE /v1/manage/tools/imports/{run}` (finished runs only), or automatically after a day. ### Import a live website (admin) Use `kind=url` with `url` (an HTTPS address with a host name) and optional `max_pages` (1 to 200, default 50). The crawl is queued for both dry run and write, so poll the run. - It stays on the start host and its `www` variant, and obeys `robots.txt` for the `AtlasImport` agent, including `Crawl-delay`. - It reads the sitemap when there is one, otherwise it follows internal links, up to depth 6. - It waits at least a second between requests and stops after 10 minutes. - Pages come in as entries with title, sanitised body, SEO tags and language. ### Import redirects from a CSV POST Upload a CSV with the columns `from, to, code` (up to 5000 rows, 2 MB). Run it with `?dry_run=1` first: the report counts `create`, `update` and `unchanged` rows and lists every refused row with a reason (not a path, unsafe target, self redirect or loop). A real run writes the valid rows, flattens redirect chains and sends the purge webhook. ### Command line importers These run on the server and need access to it, so they are for whoever operates your ATLAS install. They are the way to move a whole legacy site. | Command | What it does | | --- | --- | | `php artisan atlas:import {adapter} --brand={slug}` | Imports legacy content (`botble` or `rr-cms`) into a brand. Pass exactly one of `--dry-run` or `--write`. `--check-remote` also checks external images in the dry run. | | `php artisan atlas:import-assets {url} --brand={slug}` | Crawls a live site and takes every image it uses into the brand's media library. Pass exactly one of `--dry-run` or `--write`. | | `php artisan atlas:import-site {directory}` | Creates or updates a brand's pages, posts, fragments, menus, settings, strings and tokens from a folder of fixture files. There is no dry run, so try the folder on a scratch brand first. | ```bash php artisan atlas:import botble --brand=lifefuture --dry-run --check-remote php artisan atlas:import botble --brand=lifefuture --write ``` Rules to know: - For `atlas:import` and `atlas:import-assets`, writing needs a completed dry run of the same source first. - A brand can only be written to by an importer when it is on that importer's allowlist. Anything else is refused. - `atlas:import-assets` calls only the site host and the brand's own storage hosts, over HTTPS, and never fetches anything on another host. It reports what it skipped and why. - Content imports are idempotent: a rerun adds a revision only when the content changed, so a final delta run on cutover day is safe. A typical migration runs `atlas:import-assets` (dry run, then write), then `atlas:import-site` or `atlas:import`, then a parity check, then switches the domain. Keep the old site read only for a while and turn `404` hits into redirects. ### Exports `POST /v1/manage/tools/exports` (publisher or admin) queues a file build and answers 202. ```json { "kind": "csv", "filters": { "type": "post", "locale": "en", "status": ["published"], "from": "2026-01-01" } } ``` | Kind | Content | | --- | --- | | `json` | Entries with their latest and published revisions, terms and translation group. | | `csv` | The import columns, so it round trips with the CSV import. | | `wxr` | WordPress compatible, posts and pages only. | | `media` | `id, filename, mime, size, width, height, alt, url, created_at`. | | `redirects` | `from, to, code`. | `GET /v1/manage/tools/exports` lists runs and `GET /v1/manage/tools/exports/{id}` shows the status. When the run is `completed`, the response holds a `download` object: ```json { "download": { "url": "https://api.atlascms.id/v1/tools/exports/01m5.../download?expires=...&signature=...", "expires_at": "2026-10-21T14:47:20+00:00" } } ``` The link is signed and valid for **5 minutes**. Read the run again to get a fresh one. The file itself is private and kept for 24 hours. A link stops working while the brand is suspended. CSV cells that start with `=`, `+`, `-` or `@` get a leading quote, so a spreadsheet never runs them as formulas. Lead exports are separate: see [Forms and leads](/docs/forms-leads). ### Theme export and import `GET /v1/manage/theme/export?theme={slug}` (admin, 10 a minute) returns one bundle document for the active theme or the one you name: the theme version, its option values, and your template and part overrides. Drafts are not included. `POST /v1/manage/theme/import` (admin, 10 a minute) takes that bundle. Send `"dry_run": true` first to see what would change. ```json { "data": { "dry_run": true, "theme": { "slug": "lifefuture", "version": "0.2.4" }, "options": { "changes": 2, "unchanged": 14 }, "templates": { "created": 0, "updated": 1, "unchanged": 5 }, "parts": { "created": 0, "updated": 0, "unchanged": 2 }, "warnings": [] } } ``` A bundle is a closed document: unknown keys are refused, and every option is validated against the theme manifest. A real import drops the options draft of that theme (the report warns you) and purges the cache only when the bundle names the theme the brand is running. ### Progress in real time Import progress is broadcast on the private channel `brand.{id}.imports` as `import.progress` with `processed`, `total` and `status`. Exports, backups and other tool runs use `brand.{id}.tools` with `tool.progress`. The dashboard listens to these, so a run updates by itself. If you integrate yourself, polling the run endpoints works too. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Backups Source: https://atlascms.id/docs/backups On this page ## Backups A zip of your content as data files, with credentials and personal fields left out. - 5 min read ### What a backup is A backup is a single zip file with the content of **one brand**. It is made for safekeeping and for moving data around, so you can keep a copy outside ATLAS and download it whenever you need it. Everything on this page is for **admins**. All calls are under `https://api.atlascms.id/v1/manage` and need a signed in session and the `X-Brand-Id` header. ### There is no restore button ATLAS does not restore a brand from a brand backup through the API or the dashboard. The archive is a readable copy of your data, not a one click rollback. To undo a mistake in content, use revisions and rollback on the entry. To recover a whole install, talk to the person who operates your ATLAS server, who has platform level database backups. ### What is inside | File | Content | | --- | --- | | `manifest.json` | `schema_version`, `atlas_version`, `brand`, `created_at`, `target`, `counts` per table, `exclusions` and `media_count`. | | `{table}.jsonl` | One file per brand owned table (entries, revisions, terms, menus, settings, forms and so on), one JSON object per line. Soft deleted rows are included with `deleted_at`. | | `entry_term.jsonl` | The link between entries and terms, only for entries of this brand. | | `media.jsonl` | The list of your media files. The files themselves are **not** in the zip. | Media files stay in your media storage. The archive lists them so you know what belongs to the brand. **What is left out** Backups never contain secrets or personal data: - Anything stored encrypted or hashed, such as integration secrets, API token hashes, webhook secrets and invitation token hashes. - Mail connection username, host and password. - Personal fields of leads: submitted fields, UTM values, click ids, note, source page, referrer, IP hash, user agent, error and assignee. `manifest.json` lists every excluded column per table, so you can check exactly what is missing. ### Start a backup POST Queues a backup and answers 202 with the run. You can start up to 10 a minute. While another backup is pending or running (and younger than 2 hours) the answer is 409 with `"code": "backup_in_progress"`. ```bash curl -s -X POST "https://api.atlascms.id/v1/manage/backups" \ -H "X-Brand-Id: $BRAND_ID" --cookie "$SESSION" ``` ```json { "data": { "id": "01m5300000000000000000000b", "kind": "backup", "status": "pending", "file_name": null, "file_size": null, "download": null, "created_at": "2026-10-21T14:42:20+00:00" } } ``` The `status` moves from `pending` to `running` and then `completed` or `failed`. Progress is broadcast on `brand.{id}.tools`. A failed run carries a short `error` reason. `GET /v1/manage/backups` lists the runs, 20 per page, newest first. `GET /v1/manage/backups/{toolRun}` shows one. A run id from another brand is a 404. ### Download A completed run carries a `download` object with a signed URL and its `expires_at`. The link lasts **5 minutes**, and every read of the run mints a new one. ```bash curl -s "https://api.atlascms.id/v1/manage/backups/$RUN_ID" \ -H "X-Brand-Id: $BRAND_ID" --cookie "$SESSION" \ | jq -r '.data.download.url' \ | xargs curl -s -o backup.zip ``` The download endpoint is `GET /v1/backups/{id}/download`. An unsigned or expired link is 403, a brand that is suspended is 403, and a backup that is no longer available is 410. Downloads are limited to 30 a minute and every download is written to the audit log. Only the link expires. The archive stays until retention or you delete it with `DELETE /v1/manage/backups/{toolRun}` (204, which also removes the file). ### Settings `GET /v1/manage/backups/settings` and `PUT /v1/manage/backups/settings` read and change the schedule, retention and destination. Before the first save you get the defaults: schedule `off`, `02:00`, keep the last 5, keep 30 days, target `shared`. | Setting | Description | | --- | --- | | `schedule` | `off`, `daily` or `weekly`. Weekly runs on Monday. | | `run_at` | `HH:MM` (00:00 to 23:59) in the brand's timezone. A run starts at or after this time. | | `keep_last` | Keep the newest N completed backups (1 to 100). | | `keep_days` | Remove backups older than N days (1 to 3650). | | `target` | `shared`, `brand_storage` or `s3`. | | `endpoint`, `region`, `bucket`, `prefix`, `path_style` | For `s3`. The endpoint must be a public HTTPS address on port 443. The prefix allows letters, digits, `.`, `_`, `-` and `/`. | | `access_key_id`, `secret_access_key` | For `s3`. Write only. | ```json { "schedule": "daily", "run_at": "02:30", "keep_last": 7, "keep_days": 30, "target": "s3", "endpoint": "https://s3.example.com", "region": "auto", "bucket": "my-atlas-backups", "prefix": "atlas", "path_style": true, "access_key_id": "AKIA...", "secret_access_key": "..." } ``` The response repeats the settings with `has_access_key` and `has_secret` flags. The keys themselves are never returned. **Where the file goes** | Target | Stored in | | --- | --- | | `shared` | The platform media storage, under `tools/{brand_id}/backups/`. This is the default. | | `brand_storage` | The brand's own media storage configuration. The run fails with `backup_failed` if the brand has none. | | `s3` | Your own bucket, at `{prefix}/tools/{brand_id}/backups/{run}.zip`. Keeping a copy in a bucket you control is the safest option. | Each run remembers where it was written. If you change the target or bucket later, old backups can still be downloaded and deleted from where they are. **Test before you rely on it** `POST /v1/manage/backups/settings/test` (6 a minute) checks a destination without saving. Send the same fields as the settings, or leave them out to test the saved ones. The answer is `{ "data": { "ok", "reason", "at" } }`. When `ok` is false, `reason` is one of `credentials_missing`, `bucket_missing`, `storage_not_configured`, `endpoint_refused`, `auth_failed`, `bucket_not_found` or `unreachable`. ### Schedule and retention The scheduler checks every minute. For each brand it starts at most one backup per window: - `daily` runs once a day after `run_at`, `weekly` once a week after Monday `run_at`. - A brand with a pending, running, completed or failed backup in the current window is skipped. - A run stuck for over 2 hours is marked `failed` with reason `stale`. A failed run is retried at least an hour later, up to 3 failed attempts per window. - Retention runs after each successful backup and once a day. `keep_days` removes completed and failed runs older than the limit, `keep_last` keeps the newest completed runs. Running and pending runs are never removed. ### Size and timing A backup streams each table through a temporary file and uploads the zip as a stream, so memory use stays flat even for large brands. Runs go to the long `imports` queue with a timeout of one hour. Because media files are not inside the zip, the size follows your text content, revisions and settings, not your image library. A large brand with years of revisions can still take a few minutes, so wait for `completed` before you download. ### Security - Only admins can start, list, download, change settings or delete. Editors and publishers get 403. - Download links are signed and last 5 minutes. - S3 credentials are encrypted, write only and never appear in a response, a log or an audit row. Audit rows (`backup.started`, `backup.downloaded`, `backup.deleted`, `backup.settings.updated`) hold ids and setting names, never values. - Outbound calls to your bucket use the same guard as media storage: HTTPS, port 443, public addresses only. - Treat the zip as sensitive. It holds all your content and the email addresses of pending invitations. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Terms and Conditions Source: https://atlascms.id/terms Legal ## Terms and Conditions The rules for using AtlasCMS, written to be read. In short ### Your content is yours You keep ownership of what you publish. We only host and deliver it for you. ### Free, then by agreement One brand is free within the plan limits. Paid plans renew until you cancel. ### Use it fairly No unlawful content, spam or attacks on the service or on other brands. ### Indonesian law These terms follow the laws of Indonesia, with the competent courts in Jakarta. On this page These Terms and Conditions ("Terms") govern your use of AtlasCMS ("ATLAS", "we", "us"), a hosted content management service operated by PT Starset Teknologi Indonesia ("AtlasCMS"), based in Jakarta, Indonesia. By creating an account or using ATLAS you agree to these Terms. If you use ATLAS for a company or a client, you confirm that you may bind them. ## 1. The service ATLAS is a hosted, multi-brand headless CMS. It gives you a dashboard to manage content, a read API and software packages that connect your websites to it. The service runs on servers operated by us in Indonesia. We improve the service all the time, and features can change or be retired. We announce notable changes in the changelog. ## 2. Accounts - You need an account to use the dashboard. Give accurate details and keep them current. - You are responsible for what happens under your account, for the members you invite and for the access tokens you create. Keep passwords and tokens secret, and tell us at once if you suspect a leak. - An admin of a brand decides who joins it and with which role (editor, publisher or admin). - ATLAS is for businesses, agencies and professionals. You must be able to enter a binding agreement under Indonesian law to hold an account. ## 3. Plans and fees The Free plan covers one brand within the limits shown on the pricing page and costs nothing. A Paid plan is priced by brands and seats and is agreed with us in writing. Fees are shown at checkout or in your order. Unless the order says otherwise, a Paid plan renews automatically for the same period until you cancel, and you can cancel at any time to stop the next renewal. Fees already paid are not refunded unless the law requires it. Taxes are added where they apply. We may change the limits of a plan with notice; a change does not affect a period you have already paid for. ## 4. Your content - You own the content you and your members put into ATLAS: pages, posts, media, menus, settings and the leads your forms collect ("Your Content"). - You give us a limited licence to host, store, back up, process and deliver Your Content, only to run the service for you. - You are responsible for Your Content. You promise that you have the rights to it and that it does not break the law or the rights of others. - You can export your content at any time with the built-in tools, and you can ask us to delete it (see section 11). ## 5. Acceptable use You may not use ATLAS to: - publish content that is unlawful, defamatory, infringing, or that exploits children; - send spam, phishing or malware, or collect personal data without a lawful basis; - probe, scan or attack the service, bypass its limits or access another brand's data; - resell, sublicense or copy the service, or reverse engineer its software, except where the law allows it; - put a heavy load on the service that harms other customers. ## 6. The software licence ATLAS is proprietary software. We grant you a limited, non-exclusive, non-transferable right to use the hosted service and the client packages we publish to connect your own sites to it. The source code is not included in any plan. The full licence is on the License page. ## 7. Personal data We handle personal data as described in our Privacy Policy. For the personal data of your visitors, such as leads from your forms, you are the controller (pengendali) and we are your processor (prosesor): we process it only on your instructions to provide the service. You are responsible for telling your visitors what you collect and for having a lawful basis, including consent where it is needed. ## 8. Availability and support We work to keep ATLAS available and publish live service health at status.atlascms.id. Unless we agree a service level with you in writing, we do not promise uninterrupted service. We take regular encrypted backups. Support is by community channels on the Free plan and priority support on a Paid plan, through hello@atlascms.id. ## 9. Suspension and termination - You can stop using ATLAS at any time and ask us to close your account. - We may suspend or end access if you break these Terms, if your use puts the service or others at risk, or if the law requires it. Where we can, we tell you first. A suspended brand stops serving its public site and editor, and its content and members are kept so that it can be reactivated. - When an account ends, we delete or anonymise Your Content as described in section 11. ## 10. Disclaimers and liability ATLAS is provided "as is" and "as available". To the extent the law allows, we do not give warranties of fitness for a particular purpose or of freedom from errors. To the extent the law allows, we are not liable for indirect or consequential loss, lost profit or lost data, and our total liability for a claim is limited to the fees you paid for ATLAS in the 12 months before the claim. Nothing in these Terms limits liability that cannot be limited by law. ## 11. Export, retention and deletion You can export content in CSV or JSON with the built-in tools. When an account is closed, and whenever you ask us to, we delete Your Content and your account data within a reasonable period, except what we must keep by law. Copies in backups are removed as the backups expire, which takes at most one month. ## 12. Changes to these Terms We may update these Terms. For a material change we notify account holders by email or in the dashboard before it takes effect. If you keep using ATLAS after the date in the notice, you accept the new Terms. The date at the top of this page shows when the Terms last changed. ## 13. Governing law and disputes These Terms are governed by the laws of the Republic of Indonesia. We try to settle a dispute by talking first. If that fails, the competent courts in Jakarta have jurisdiction, unless the law gives you a different right. ## 14. Contact Questions about these Terms: hello@atlascms.id. ### Questions about these terms? Write to the team and we will answer in plain words. Email hello@atlascms.id --- # Privacy Policy Source: https://atlascms.id/privacy Legal ## Privacy Policy What we collect, why, who sees it and how you stay in control. In short ### We do not sell personal data Data is shared only with providers that help run the service, under data protection duties. ### Stored in Indonesia The service and its backups run on servers operated by us in Indonesia. ### You stay in control UU PDP gives you the right to access, correct, delete and withdraw consent. ### Analytics only with consent Analytics cookies on this website are set only after you accept them. On this page This Privacy Policy explains how AtlasCMS ("ATLAS", "we") collects and uses personal data when you use our website and our hosted CMS. The operator is PT Starset Teknologi Indonesia, Jakarta, Indonesia. We follow Indonesian Law No. 27 of 2022 on Personal Data Protection (UU PDP) and its implementing rules. ## 1. Two roles - For our own account data (people who sign up and use the dashboard) we are the data controller (pengendali data pribadi). - For the content and leads of a brand (for example form submissions from a customer's website visitors) the customer is the controller and we act as the processor (prosesor) on their instructions. If you are a visitor of a customer's website, please read that site's own privacy notice. ## 2. What we collect DataWhy we process it Account: name, email address, password (stored only as a hash), profile picture, role in each brandTo create your account, sign you in and manage access. Sign in with Google or GitHub, when you choose it: provider id and verified email. We store no provider access tokens.To let you sign in and to match you to your existing account. Brand content you create: pages, posts, media, menus, settings, translationsTo host and deliver your content. Leads from your forms: names, email addresses, phone numbers, campaign ids, the page they came fromTo give the brand its leads, and to forward them to the CRM the brand connects. Site analytics: page path, referrer, a visitor identifier. Ad and conversion events are sent to platforms such as Meta or TikTok only when the brand turns them on and the visitor consents.To show the brand its traffic. Technical data: IP address, browser, request ids and error reportsTo keep the service secure, apply rate limits and fix faults. Error reports have personal data removed before they are stored. Audit trail: who changed what and when, recorded with ids and never with content bodies or secretsSecurity and accountability for brand teams. Messages you send us, for example to hello@atlascms.idTo answer you. ## 3. Cookies and similar storage We use three kinds of cookies and similar storage: - Necessary cookies. The dashboard uses a session cookie and a CSRF cookie that keep you signed in and protect your requests, and this website remembers your cookie choice. They are strictly necessary and do not need consent. If you tick "Remember me" the session stays on that device for longer. - Analytics cookies. This website, atlascms.id, uses analytics cookies to understand how the site is used. They are set only with your consent, and nothing is loaded before you accept. - Your choice. The cookie banner appears on your first visit. Choose Accept, Decline or Customize. You can reopen it at any time with "Cookie settings" in the footer, and change or withdraw your consent. Websites that customers build with ATLAS set their own cookies and show their own consent banner. ## 4. Legal bases We process personal data on these bases: your consent, where you give it; performance of our agreement with you; our legitimate interests in securing and improving the service, where your rights do not outweigh them; and compliance with legal obligations. ## 5. Who receives your data (subprocessors) We do not sell personal data. We share it only with providers that help us run the service, and only as far as needed: - Hosting and storage. The service and its backups run on servers operated by us in Indonesia. A brand may choose its own S3 compatible storage for media. - Infrastructure and service providers. ATLAS runs its own email delivery and its own servers in Indonesia. We may use trusted infrastructure and service providers to operate the service, always under confidentiality and data protection obligations. A brand can connect its own SMTP server for its own emails. - Sign in. Google and GitHub, only when you choose to sign in with them. - AI drafting. AI drafting features use third-party AI models through processors bound by contract. When a member uses an AI feature, the text they ask us to process is sent to those processors. AI output stays a draft until a person publishes it. - Error monitoring and integrations. Error reports have personal data scrubbed before they are stored. When a brand connects a CDN or turns on advertising and conversion tracking, data goes to the services the brand chose, on the brand's instructions. Some of these providers process data outside Indonesia. In that case we rely on the safeguards that UU PDP requires. ## 6. How long we keep data - Account data: while your account is active and for a short period afterwards to settle disputes. - Brand content and leads: until the brand deletes them or asks us to. Deleting a lead removes the record. - Audit logs: for as long as the account is active, unless the customer asks otherwise. - Backups: encrypted, kept for at most one month on a rolling basis, and by default they leave out personal data. Longer backup retention is available on request as a paid storage add-on of up to Rp800.000. ## 7. Security Every brand is isolated from the others. Secrets such as integration keys are stored encrypted and are never shown again. Webhooks are signed. See the Security page for more. No system is perfectly secure; if a breach affects your personal data we will notify you and the authority within the time UU PDP requires (3 x 24 hours). ## 8. Your rights Under UU PDP you have the right to: know how your data is used, access and receive a copy of it, correct or complete it, delete or destroy it, withdraw your consent, object to automated decisions, restrict processing, claim compensation for a breach of your rights, and move your data to another service where this applies. To use a right, write to protection@atlascms.id. We may ask you to prove who you are, and we answer within the time the law sets. If you are a visitor of a customer's website, contact that customer first. ## 9. Children ATLAS is for businesses and agencies and is not directed at children. We do not knowingly collect personal data from children. If you believe a child has given us personal data, write to us and we will delete it. ## 10. Changes We update this policy when our practices change. The date at the top shows the latest version. For a material change we tell account holders in advance. ## 11. Contact Data protection questions and requests: protection@atlascms.id, our data protection contact. For anything else write to hello@atlascms.id. Company: PT Starset Teknologi Indonesia, Jakarta, Indonesia. ### Data protection contact Requests about your personal data go to our data protection contact. protection@atlascms.id --- # License Source: https://atlascms.id/license License ## Proprietary, free within the limits. ATLAS is proprietary software made by PT Starset Teknologi Indonesia. Free to use within the Free plan, with the source code kept private. See pricing Read the terms Compare ## What each plan lets you do - Free - Paid ### Use Use ATLAS for business sites Read API and client packages for your own sites Export your content in CSV or JSON Brands More brands ### Beyond the limits Review workflow and roles per brand Priority support Source code ### No source code The source of ATLAS is not shared on any plan, free or paid. ### No copying or reselling Do not copy, modify, sublicense, resell or host ATLAS for others, or reverse engineer it, except where the law allows. ### No working around limits Plan limits stay in place without a Paid plan. ### Packages and open source The client packages are licensed only to connect your own sites. Open source components keep their own licences. Not included ## What the license does not cover The short list, in plain words. ## Need a custom agreement? For an agency arrangement or any licence question, write to us. Pricing Email hello@atlascms.id --- # Security Source: https://atlascms.id/security Security ## Built so brands stay apart. Isolation, access, secrets, an audit trail and backups, in plain words. Security guide for developers Privacy Policy ### Brand isolation Every record belongs to one brand and every request is checked against it. A record of another brand answers "not found". ### Roles per brand Editors write drafts, publishers decide what goes live, admins manage settings. Every brand keeps at least one admin. ### Encrypted secrets Integration keys and passwords are stored encrypted and are never shown again, not in the dashboard, the API or a log. ### Signed webhooks Every message carries an HMAC-SHA256 signature and a timestamp, so your site can refuse a fake or a replay. ### Audit trail Changes to live content are recorded with who and when, using ids and never content or secrets. Revisions let you roll back. ### Backups Database backups are encrypted, kept for at most one month on a rolling basis, and leave out personal data by default. Longer retention is a paid add-on on request. ### Verified restores Backups are checked by restoring them in a scratch database, and a failed run shows up in our console. ### Safe content Links are checked on save and when served, rich text and SVG uploads are cleaned, and uploads are verified. Controls ## What protects your content The controls that run on every brand, all the time. ### Where your data lives ATLAS runs on servers operated by us in Indonesia. Providers that help us run the service work under confidentiality and data protection duties. Read the Privacy Policy ### Sign in and sessions Passwords are stored as hashes. You can sign in with Google or GitHub on an account that already exists. Changing your password signs out your other devices. ### AI stays a draft AI drafting uses third-party models through processors bound by contract. Its output is a draft until a person publishes it. ### Report a problem Found a vulnerability? Tell us first. We review every report and tell you when it is fixed. protection@atlascms.id Data ## How we handle data Where it lives, who touches it and how to reach us. ## Data protection and security contact Questions about your data, a request under UU PDP or a security report: protection@atlascms.id. Privacy Policy Email protection@atlascms.id --- # About Source: https://atlascms.id/about About ## Software for teams One dashboard for every brand, one fast read API for every website. that run many brands. Start free Read the docs Brands run on ATLAS ``` releases ``` Releases shipped Languages, English and Indonesian Of public service history on the status page ### One admin for every brand A team that runs five sites should not run five CMSs. Brands share one dashboard and stay fully apart in the data. ### One fast API Every website reads through one cached read API and a typed SDK. Build with Nuxt, or anything that can fetch. ### Brand isolation Every record, token and setting belongs to one brand. Revisions, review and audit keep a decision trail. ### Built in Indonesia Made in Jakarta by PT Starset Teknologi Indonesia, with servers in Indonesia and an English and Indonesian interface. What we believe ## Four ideas behind ATLAS Our story ## From our own brands to yours. ATLAS started as the content platform behind the brands of Life Future. Running several sites showed what a team needs every day: reviews before anything goes live, translations that stay linked, redirects that never break, forms that turn into leads, and a history that lets you undo a mistake. We ship every few days and write down what changed in the changelog. Now we offer ATLAS to other companies and agencies that run many brands, with a free plan for the first one. Questions, a demo for your team or a partnership: write to hello@atlascms.id. Something not working? Check status.atlascms.id. ATLAS, the content platform - Life Future - Takumi - HX - Netcable Your brand ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Talk to us - Free for one brand - No credit card - Upgrade anytime --- # Contact Source: https://atlascms.id/contact Contact ## Talk to the ATLAS team. Questions about plans, a demo for your team, a partnership or a problem: we reply within one working day. ### Sales and plans Brands, seats and the review workflow. Tell us about your team. hello@atlascms.id ### Support Something not working or unclear? Send the request id from the error page. hello@atlascms.id ### Data protection and security Requests about your personal data, and security reports. protection@atlascms.id ### Service status Live health of the API, database, queue and scheduler. status.atlascms.id ## Before you write to us. The quick answers. All questions Read the docs How fast do you reply? Within one working day. Paid plans get priority support. Who runs AtlasCMS? AtlasCMS is operated by PT Starset Teknologi Indonesia in Jakarta, Indonesia, and built by the team at Life Future. How do I report a security problem? Write to protection@atlascms.id with what you found and how to reproduce it. We review every report and tell you when it is fixed. How do I ask for my data to be deleted? Write to protection@atlascms.id. We delete your data within a reasonable period, and copies in backups expire within one month. ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Talk to us - Free for one brand - No credit card - Upgrade anytime --- # Pricing Source: https://atlascms.id/pricing Pricing ## Start free. Pay when you need more. One brand is free for as long as you like. When your team needs more brands, seats and a review workflow, move to Paid. Start here Rp 0 forever, for one brand Start free - One brand to try the platform - Public read API and the admin - Block pages, revisions and translations - Community support For teams Most teams Talk to us priced by brands and seats Contact sales - More brands and seats for teams - Review workflow and roles for every brand - Everything in Free, without limits - Priority support Compare ## What each plan includes - Free - Paid ### Usage Brands More brands Team seats Limited More seats ### Content Block-based pages, posts and products Revisions and rollback Translations Media library ### Team Review workflow (request, approve, publish) Roles per brand (editor, publisher, admin) Audit trail ### Delivery Public read API and SDKs Signed webhooks ### Support & license Support Community Priority Source code License ## Proprietary, in plain words ATLAS is proprietary software made by AtlasCMS. Here is what that means for you. ### You can - Use ATLAS free, also for business sites, within the Free limits - Connect any front end through the read API - Export your content any time, in CSV or JSON ### Paid unlocks - More brands and seats - Review workflow and roles per brand - Priority support from the ATLAS team ### Not included - Source code, in any plan - Copying, modifying or reselling ATLAS - Removing the plan limits without a paid plan Read the full license terms ## Questions, answered. Everything that usually comes up before a team picks a plan. Read the docs Write to hello@atlascms.id Is ATLAS open source? No. ATLAS is proprietary software made by AtlasCMS. You can use it for free within the Free plan limits, but the source code is not shared, not on Free and not on Paid. Is the Free plan really free? Yes. There is no trial period and no credit card. You keep using it for free for as long as you stay within the Free plan limits. Can I use the Free plan for a business website? Yes. The Free plan is not only for testing. You can run one live brand on it for as long as you like, as long as you stay within its limits. What happens when I need a second brand? Move to the Paid plan. Paying removes the limits, so you can add more brands and invite more of your team. Your content stays where it is. Do I get the source code on a paid plan? No. Paying unlocks every feature without limits, but the source code stays with AtlasCMS. Can I host ATLAS on my own server? No. ATLAS is hosted by AtlasCMS, so you never have to run servers, updates or backups yourself. Can I take my content with me if I leave? Yes. You can export your content any time, in CSV or JSON. ## Not sure which plan fits? Tell us how many brands and editors you have. We reply within one working day. Read the docs Contact sales --- # Core concepts Source: https://atlascms.id/docs/concepts On this page ## Core concepts The few ideas that everything else in ATLAS is built from. - 4 min read ### Brands A brand is one website with its own content, media, menus, settings, theme, members and tokens. One ATLAS account can run many brands, and nothing is shared between them: every query the API runs is scoped to the active brand. A record id that belongs to another brand answers **404**, never 403, so a stranger learns nothing about what exists. A brand is found from the token on a request, or from the domain it serves when a call carries no token. A suspended brand answers 403 on both the public and the dashboard API. ### Entries and content types Everything you publish is an entry. A page and a post are entries, and so is each record of a custom content type you define, such as a city, a product or a release. An entry has a language, a type and a path, and that triple is unique inside a brand. A custom content type is a small schema of typed fields: text, number, date, link, media, rich text, a list, a repeater of rows, or a reference to another entry. The dashboard builds its form from the schema and the API checks every save against it. Entries can be filed under **terms** (categories and tags), and a page can sit under a parent page, which makes its path nest. ### Revisions An entry never changes in place. Each save adds a revision with the title, blocks, SEO and fields, and the newest revision is the draft. Publishing points the entry at one revision, and a rollback points it back at an earlier one. Nothing is copied. Revisions are append-only, so you always have the history and a diff between any two. The API keeps the last 50 revisions of each entry, plus the published one, the one a schedule points at, and any revision an open review or a translation depends on. ### Publish, schedule and rollback Only a **publisher** or an **admin** can change what is live. Publishing can name the exact revision to publish; if the page has moved on since, the API answers **409** `revision_changed` instead of publishing something nobody reviewed. - **Publish** makes the chosen revision live at once. - **Schedule** publishes it at a time you pick. A scheduler checks every minute. - **Rollback** makes an earlier revision the live one again. - **Review** lets an editor ask for approval. A publisher approves and the reviewed revision goes live exactly as it was reviewed. Every change to live output writes an audit row, raises the brand content version and queues the signed [purge webhook](/docs/webhooks), so your site refreshes within about a second. ### Translations A translation is another entry in another language, linked to the first through a translation group. Paths are per language, and the links between them come out as `translations` in the page answer, which is what a site needs for hreflang. When a page has no entry in the requested language, the API serves the default language and says so with `"fallback": true`, unless the brand turned that fallback off. A translation counts as outdated when the default-language entry has a newer revision than the one it was made from. ### Blocks and themes A page body is a tree of **blocks**: a hero, a feature grid, an FAQ, a form. Blocks nest at most three levels deep (section, column, block). Media is stored as an id and the API resolves it to URLs in the answer. A block definition is the single source of truth for a block: it drives the editor form, the server validation and the component that renders it. A **theme** is the package that turns blocks into a website: block components, page templates, header and footer parts, and options a brand can change without code. Each brand has a token set (colours, fonts, radius) that the public API serves from `/v1/public/theme`, so a site and the dashboard preview read the same values. ### Your site only ever reads A brand site reads published content through the public API. Writes, including lead forms, go through the layer's own server route, which adds the token on the server. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Public API reference Source: https://atlascms.id/docs/api On this page ## Public API reference Everything under /v1/public: what you send, what comes back and how it is cached. - 5 min read ### Overview The public API is read-only apart from three small write endpoints (lead forms, 404 logging and analytics). Every path below is relative to `https://api.atlascms.id`. | Topic | Rule | | --- | --- | | Base path | `/v1/public` | | Authentication | `Authorization: Bearer atl_read_...`, or the `X-Atlas-Token` header. Without a token the brand comes from the `Host` header. | | Envelope | Single resources and plain lists come back as `{ "data": ... }`. The entries list is a cursor page with `data`, `links` and `meta`. | | Content | Only published content. A preview token also reaches drafts, and its responses are `private, no-store`. | | Locale | Pass `locale` to pick a language. The brand default is used when it is left out. | ```bash curl -s "https://api.atlascms.id/v1/public/settings" \ -H "Authorization: Bearer $ATLAS_TOKEN" | jq '.data.brand.slug' ``` ### Errors | Status | Meaning | | --- | --- | | 401 | The token is missing where one is required, invalid, expired or revoked. More than 30 invalid tokens a minute from one address answers 429. | | 403 | The brand is suspended, or a custom content type needs a higher role than a public caller has. | | 404 | There is no published entry at that path, the locale is not one of the brand's, or a record belongs to another brand. | | 422 | A required parameter is missing or invalid, for example a page request without `path`. The body is `{ "message", "errors": { "field": ["..."] } }`. | | 429 | Too many requests. See rate limits below. | Every response carries an `X-Request-Id` header (`req_` and ten hex characters). Quote it when you write to support. ### Caching and ETags Every response carries an `ETag` and a `Cache-Control` header, and a matching `If-None-Match` is answered **304** without a body. For `page`, `route`, `entries` and `sitemap-urls` the ETag is derived from the brand content version, so a 304 is answered before any rendering. A publish, or any change that alters live output, raises the content version and so invalidates every older ETag at once. ```bash URL="https://api.atlascms.id/v1/public/page?path=/&locale=en" ETAG=$(curl -s -D - -o /dev/null "$URL" -H "Authorization: Bearer $ATLAS_TOKEN" \ | grep -i '^etag:' | cut -d' ' -f2- | tr -d '\r') curl -s -o /dev/null -w '%{http_code}\n' "$URL" \ -H "Authorization: Bearer $ATLAS_TOKEN" -H "If-None-Match: $ETAG" # 304 ``` | Header | Value | | --- | --- | | `Cache-Control` | `max-age=60, public, stale-while-revalidate=300` on delivery responses. Preview responses are `private, no-store`. | | `ETag` | `"cv{version}-{window}-{hash}"` for page, route, entries and sitemap-urls. A hash of the body for the other endpoints. | | `Vary` | `Authorization, X-Atlas-Token` | A short server-side body cache (120 seconds by default) sits behind the ETag; a content version bump makes every older body unreachable. The [SDK](/docs/sdk) and the [Nuxt layer](/docs/nuxt) revalidate with ETags for you. ### Rate limits | Call | Limit | | --- | --- | | Reads with a token | 6000 a minute per token | | Reads without a token | 600 a minute per host and address | | `POST forms/{key}` | 10 a minute per token and visitor address | | `POST beacon` | 300 a minute per token and visitor address | | `POST events` | 600 a minute per token and visitor address | A site that calls the API from its server should pass the visitor's address in `X-Atlas-Client-Ip` and `X-Atlas-Client-User-Agent`, so the visitor is judged and not the server. The API believes these headers only on a call that carries a read token. ### Page GET The published page at a path: blocks, SEO, translation links, the route that matched and the theme that renders it. | Parameter | Description | | --- | --- | | `path` | Required. The entry path, up to 255 characters. `/` returns the front page the brand chose in Reading settings. | | `locale` | Optional. A language of the brand. | | `type` | Optional. Restrict the lookup to one entry type. | Response fields: `id`, `type`, `locale`, `path`, `is_front_page`, `template`, `title`, `seo`, `fields` (the typed fields of a custom type), `terms`, `published_at`, `modified_at`, `blocks`, `translations` (`[{ locale, path }]`), `fragments`, `route`, `theme` and `fallback`. A 404 means there is no published page. When the brand lists the latest posts at the front, `path=/` is a 404 and the route endpoint answers instead. ```bash curl -s "https://api.atlascms.id/v1/public/page?path=/pricing&locale=en" \ -H "Authorization: Bearer $ATLAS_TOKEN" ``` ### Route GET The `route` and `theme` blocks alone, for a path the page endpoint answers 404 for: an archive, the posts page, search, a not found page, or the locale root when it lists the latest posts. An unknown locale is a 404. | Parameter | Description | | --- | --- | | `path` | Required. The path to resolve. | | `locale` | Optional. | | `q` | Optional. The search text, for a search route. | ```json { "data": { "path": "/search", "locale": "en", "route": { "kind": "search", "template": "index", "candidates": ["search", "index"] }, "theme": { "slug": "atlascms", "version": "0.1.0", "template": { "slug": "index", "source": "package", "blocks": null }, "parts": { "header": { "slug": "header", "area": "header", "source": "package", "blocks": null } } } } } ``` ### Entries GET A cursor-paged list of published entries of one type: posts, pages or a custom type. | Parameter | Description | | --- | --- | | `type` | The entry type, such as `post`, `page` or a custom key. | | `locale` | A language of the brand. | | `term`, `taxonomy` | Filter by a term slug. `taxonomy` is `category` or `tag` and says which one the slug means. | | `limit` | 1 to 100. The default is 25, or the brand's posts per page for `type=post`. | | `cursor` | The `meta.next_cursor` of the previous page. | | `q` | Text search, up to 100 characters, over the path, title, excerpt and SEO description. | | `fields` | Omitted: no blocks, with SEO. `light`: no blocks and only the share image in SEO, plus `author_name` for posts. `full`: adds `blocks`. | | `select` | A comma separated list of at most 12 fields, such as `title,path,fields.version`. It wins over `fields`. An unknown field is a 422. | | `count` | `1` adds `meta.total`, the number of matches ignoring the cursor. | | `filter[field]` | Equality on a declared field of a custom type, up to 5. | ```bash curl -s "https://api.atlascms.id/v1/public/entries?type=post&limit=10&count=1&fields=light" \ -H "Authorization: Bearer $ATLAS_TOKEN" ``` ```json { "data": [ { "id": "01m5ffh6...", "type": "post", "locale": "en", "path": "/blog/hello", "title": "Hello", "seo": { "og_image": null }, "author_name": "Sari", "fields": {}, "terms": [], "published_at": "2026-10-21T14:58:33.000000Z" } ], "links": { "first": null, "last": null, "prev": null, "next": "https://.../v1/public/entries?type=post&cursor=eyJ..." }, "meta": { "per_page": 10, "next_cursor": "eyJ...", "prev_cursor": null, "total": 6 } } ``` Walk the list by sending `meta.next_cursor` as `cursor` until it is `null`. ### Menus GET A navigation menu such as `header` or `footer`. Items form a tree. Each item has a `label` and may have `url`, `key`, `eyebrow`, `description`, `attributes` and an `image` (`{ url, alt, width, height }`). `locale` picks the language; an unknown key is a 404. ```json { "data": { "id": "01m518vv88kdjb68enh6w74ded", "key": "header", "locale": "en", "items": [ { "key": "features", "label": "Features", "url": "/features" }, { "key": "pricing", "label": "Pricing", "url": "/pricing" } ] } } ``` ### Settings GET The brand and the settings the registry marks public. The answer carries `brand` (`name`, `slug`, `default_locale`, `locales`), `settings`, `branding` (logo, favicon, share image), and the resolved `reading` and `permalinks` objects. Private keys never appear. A preview token also gets `brand.slug`, which the Nuxt layer compares before it honours a preview cookie. ### Theme GET The brand token set and the resolved theme options for one locale. `tokens` holds the colours with their scales, fonts and radius, and `options` holds what the theme exposes for editing. A preview token receives the draft options. | Parameter | Description | | --- | --- | | `locale` | Optional. The language of the option texts. | ### Strings GET Interface texts the brand manages, grouped by namespace. | Parameter | Description | | --- | --- | | `locale` | Optional. | | `namespace` | Optional. Only this group, such as `site`. | ```json { "data": { "locale": "en", "strings": { "site": { "copyright": "AtlasCMS.", "language": "English" } } } } ``` ### Terms GET Categories and tags, each as `{ taxonomy, slug, name, path }` where `path` is the archive path from the brand's permalink bases. | Parameter | Description | | --- | --- | | `taxonomy` | `category` or `tag`. | | `slug` | One term. | ### Content types GET The custom content types the brand serves, each with its field list so a site does not have to parse the schema. Pass `locale` to get labels in one language. ### Sitemap URLs GET Every public path with its language, last change and the alternates for hreflang: `[{ path, locale, type, lastmod, alternates: [{ locale, path }] }]`. ### Redirects GET The redirect map as `[{ from, to, code }]`. Moving a published entry adds a 301 here for you. ### Machine readable files GET The data for `llms.txt`, as JSON, or as text with `format=text`. Takes `locale`. GET The IndexNow key file. A 404 when IndexNow is not enabled for the brand. GET The web app manifest of the brand: names, icons (192, 512 and maskable), theme colour, `display: standalone` and `start_url`. ### Submit a form POST Sends a lead from a form the brand defined. Only the fields the form declares are accepted and stored. The form has to be active, and the Forms module has to be on. | Field | Description | | --- | --- | | `fields` | Required object with one entry per form field, checked against the form definition (email, phone, select and length rules). | | `consent` | Optional boolean. | | `utm`, `fbclid`, `gclid` | Optional campaign attribution. | | `source_page`, `referrer`, `locale` | Optional context. | | `website` | A honeypot. Leave it empty; a filled value gets a 202 and nothing is stored. | ```bash curl -s -X POST "https://api.atlascms.id/v1/public/forms/contact" \ -H "Authorization: Bearer $ATLAS_TOKEN" -H "Content-Type: application/json" \ -d '{"fields":{"name":"Sari","email":"sari@example.com"},"consent":true,"source_page":"/contact"}' ``` Success is **201** with `{ "data": { "id", "message" } }`. An unknown or inactive form is a 404, and invalid fields are a 422. ### Log a not found page POST Lets a site record a 404 so editors see what visitors miss in the dashboard and can add a redirect. ### Analytics beacon POST Counts a page view. Send `path` (starts with `/`, up to 512 characters), and optionally `referrer` and a `visitor` id of 8 to 64 characters. The answer is 204 with no body. ### Tracking events POST Sends a tracking event to the ad platforms the brand connected. It needs a read token. A `browser` event may only be `PageView`, `ViewContent` or `Lead`, and `custom_data.value` and `custom_data.currency` are dropped. Purchase style events and money need `source: "server"`, which only a call with a read token can send. | Field | Description | | --- | --- | | `event_name`, `event_id` | Name and a unique id, so a browser and a server copy of one event count once. | | `source` | `browser` (default) or `server`. | | `email`, `phone`, `external_id` | Matching data for the platform. | | `fbp`, `fbc`, `ttclid` | Click and browser ids. | | `client_ip`, `client_user_agent`, `source_url`, `custom_data` | Context and extra data. | ### Status GET Public health of the platform, with no token: `status` (`operational`, `degraded`, `outage` or `unknown`), `generated_at`, the API version and a list of `services` (`database`, `redis`, `queue`, `scheduler`) with 90 days of history. Cached for 10 seconds. The same data feeds [status.atlascms.id](https://status.atlascms.id). ### The complete reference The API serves a generated reference of every endpoint, including the dashboard API, at `/docs/api` on its own address. Use this page as the guide and that one as the full list. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Tokens and security Source: https://atlascms.id/docs/security On this page ## Tokens and security Which credential to use for what, who may do what, and what ATLAS never lets through. - 2 min read ### Token kinds A token belongs to one brand. The plain value is shown **once**, when the token is created, and ATLAS keeps only a hash. Only an admin can create or revoke tokens, apart from short preview tokens. | Kind | Reads | Who creates it | Expiry | | --- | --- | --- | --- | | `read` | Published content through `/v1/public` | Admin | Optional, 1 to 43200 minutes. Without one it never expires. | | `preview` | Drafts too, never cached | Admin, or any member for the editor's own preview | Required. Up to 7 days for an admin, up to 2 hours for a member. | | `agent` | A scoped door for an outside agent (read, and draft-only writes) | Admin, when the brand allows agents | 15 minutes to 90 days, 30 days by default. | | `developer` | Pushes a theme from the `atlas` command line | Admin | 15 minutes to 90 days, 30 days by default. | - Keep read tokens in server-side environment variables. Never ship one in browser code or commit it. - Rotate by creating a new token, deploying it and then revoking the old one. - A preview token must expire. Give it the shortest life that fits the review. - An agent token never publishes: its writes are drafts that a person approves. ### How to send a token ```bash # Either header works curl -s "https://api.atlascms.id/v1/public/page?path=/" -H "Authorization: Bearer $ATLAS_TOKEN" curl -s "https://api.atlascms.id/v1/public/page?path=/" -H "X-Atlas-Token: $ATLAS_TOKEN" ``` A token in the query string is never accepted. A site that calls from its server may add `X-Atlas-Client-Ip` and `X-Atlas-Client-User-Agent` so rate limits judge the visitor, not the server. ### Roles Every member of a brand has one role. The API checks it before it looks at the request body. | Role | Can | | --- | --- | | Editor | Write drafts, request a review, upload media, create terms, and move pages that are not live. | | Publisher | Everything an editor can, plus publish, schedule, roll back and delete, change live pages, redirects, menus, fragments and strings, and read and manage leads. | | Admin | Everything a publisher can, plus settings, theme, members, tokens, integrations, secrets, imports and the audit log. | A custom content type can set a minimum role. A caller below it gets 403 on that type's entries. A brand always keeps at least one admin. ### What ATLAS enforces - **Brand isolation.** Every brand-owned row is scoped to its brand and the scope fails closed. A record id from another brand is a 404. - **Encrypted secrets.** Integration keys and webhook secrets are write-only: they are stored encrypted and are never returned, logged or put in a URL. - **Audit trail.** Every change to live output writes an audit row with ids, never content bodies or secrets. - **Safe links and markup.** Every stored link is checked on save and again on output, and rich text is cleaned on save. - **Safe outbound calls.** Webhooks, CRM forwards and CDN purges go over HTTPS to public addresses only, never to private networks. - **Signed webhooks.** Every delivery is signed with HMAC-SHA256. See [webhooks](/docs/webhooks). - **AI is a draft.** AI output lands as a draft revision. The one exception is an autopost setting an admin switches on, which is off by default and audited. The plain-language summary lives on the [Security page](/security). To report a vulnerability, write to [hello@atlascms.id](mailto:hello@atlascms.id). ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Webhooks and purge Source: https://atlascms.id/docs/webhooks On this page ## Webhooks and purge How your site learns that content changed, and how to prove the message came from ATLAS. - 2 min read ### How it works When an editor publishes, unpublishes, moves, rolls back or deletes an entry, or changes something site-wide such as a menu, the theme or the settings, ATLAS queues one signed `POST` for every active webhook of the brand. Your site checks the signature, drops its cached copy of the affected pages and the next visitor gets fresh content, about a second after the publish. Add a webhook under Settings, Publishing, Hooks. Give it the full HTTPS address of your endpoint and a secret. The URL must use port 443 and resolve to a public address. Each delivery shows up in the Delivery list, where a failed one can be retried. ### The request | Header | Value | | --- | --- | | `Content-Type` | `application/json` | | `X-Atlas-Event` | The event name, for example `entry.published`. | | `X-Atlas-Delivery` | The delivery id. The same id is sent again on a retry. | | `X-Atlas-Timestamp` | Unix time in seconds when the message was signed. | | `X-Atlas-Signature` | `sha256=` followed by the hex HMAC. | ```json { "event": "entry.published", "brand": "atlascms", "entry": { "id": "01m518vvbf8txh40wcvwhvtzyd", "type": "page", "locale": "en", "path": "/faq" }, "paths": ["/faq"], "at": "2026-10-21T14:42:20+00:00" } ``` `paths` lists the public paths to refresh. A change that affects the whole site (`settings.updated`, `menu.updated`, `theme.activated` and similar) sends `"paths": ["*"]` and no `entry`. A manual purge from Tools, Cache sends the paths you chose. | Event | When | | --- | --- | | `entry.published`, `entry.updated` | An entry went live or its live output changed. | | `entry.unpublished`, `entry.deleted` | An entry left the public site. | | `entry.moved`, `entry.rolled_back` | A path changed, or an earlier revision became live. The old path is in `paths` too. | | `settings.updated`, `menu.updated`, `fragment.updated`, `strings.updated` | Site-wide content changed. `paths` is `["*"]`. | | `theme.updated`, `theme.activated`, `theme.options.published` | The theme or its options changed. `paths` is `["*"]`. | | `redirect.created`, `term.updated`, `media.replaced`, `import.completed` | Other changes to what the public site shows. | ### Verify the signature The signature is `HMAC-SHA256` of the string `{timestamp}.{raw body}` with your webhook secret, written as hex. Always compute it over the **raw** body bytes you received, before any JSON parsing, compare in constant time, and refuse a timestamp that is more than five minutes old. ```typescript import { createHmac, timingSafeEqual } from 'node:crypto' export function isFromAtlas(rawBody: string, headers: Headers, secret: string): boolean { const timestamp = headers.get('x-atlas-timestamp') ?? '' const age = Math.abs(Date.now() / 1000 - Number(timestamp)) if (!Number.isFinite(age) || age > 300) return false const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest() const given = Buffer.from((headers.get('x-atlas-signature') ?? '').replace(/^sha256=/, ''), 'hex') return given.length === expected.length && timingSafeEqual(given, expected) } ``` ```php $timestamp = $_SERVER['HTTP_X_ATLAS_TIMESTAMP'] ?? ''; $given = str_replace('sha256=', '', $_SERVER['HTTP_X_ATLAS_SIGNATURE'] ?? ''); $rawBody = file_get_contents('php://input'); $fresh = abs(time() - (int) $timestamp) <= 300; $expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret); if (! $fresh || ! hash_equals($expected, $given)) { http_response_code(401); exit; } ``` ```bash # Reproduce a signature by hand while you debug printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" ``` ### Parse after you verify Reading the body as JSON and serialising it again can change spaces and key order, and the signature will no longer match. Verify the raw string first. ### Answer fast, expect repeats - Return any **2xx** within a few seconds. Do the cache work after you answer if it is slow. - Any other answer, or a timeout, counts as a failure. ATLAS tries **5 times** in total, waiting 10 seconds, 1 minute, 5 minutes and 15 minutes between tries. After the fifth failure the delivery is marked failed and admins are notified. - Delivery is at least once. Make your handler safe to run twice, and use `X-Atlas-Delivery` to spot a repeat. - Use a different secret for every site. A secret decides which brand's cache a signed message may purge. ### With the Nuxt layer `@atlas/nuxt` ships the endpoint for you. Point the webhook at `https://your-site.example/api/__atlas/purge` and set the same secret in `NUXT_ATLAS_PURGE_SECRET`. The route verifies the signature and the five minute window, then drops the cached CMS data of that brand and answers `{ "brand", "purged" }`. A bad or stale signature is a 401. A multi-brand site gives each host its own `purgeSecret`; see the [layer guide](/docs/nuxt). ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # @atlas/client Source: https://atlascms.id/docs/sdk On this page ## @atlas/client A typed fetch client for /v1/public with ETag revalidation. Works in Node 22 and any runtime with fetch. - 1 min read ### Install The four `@atlas/*` packages share one version and are published privately to GitHub Packages as `@lifefuturedev/atlas-*`. Write to [hello@atlascms.id](mailto:hello@atlascms.id) to get access for your team, then point the scope at GitHub Packages and alias the package. ```bash @lifefuturedev:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} ``` ```json { "dependencies": { "@atlas/client": "npm:@lifefuturedev/atlas-client@0.1.1" } } ``` ### Create a client ```typescript import { createAtlasClient } from '@atlas/client' export const atlas = createAtlasClient({ baseUrl: 'https://api.atlascms.id', token: process.env.ATLAS_TOKEN!, timeoutMs: 8000, }) ``` | Option | Description | | --- | --- | | `baseUrl` | Required. The address of the API, with no trailing path. | | `token` | Required. A read token. | | `previewToken` | Optional. Replaces the token, makes drafts reachable and turns the cache off. | | `fetch` | Optional. Your own `fetch`. | | `cache` | Optional. An ETag cache to share. The default is an in-memory LRU of 500 responses. | | `timeoutMs` | Optional. Abort a request after this long. | ### Methods | Method | Returns | | --- | --- | | `getPage({ path, locale })` | The page, or `null` on a 404. Media is already resolved. | | `getRoute({ path, locale })` | The route and theme of a path the page endpoint does not answer, or `null`. | | `getEntries({ type, locale, cursor, limit, term, taxonomy, fields, count })` | `{ data, next_cursor, count }`. | | `getMenu(key, locale)` | A menu with its items sanitised, or `null`. | | `getTerms({ taxonomy, slug })` | A list of terms with their archive paths. | | `getSettings()` | The brand settings the registry marks public, or `null`. | | `getTheme({ locale })` | `{ tokens, options }`. | | `getStrings({ locale, namespace })` | Interface strings, or `null`. | | `getSitemapUrls()`, `getRedirects()` | Lists for a sitemap and a redirect map. | | `getLlms(locale)`, `getLlmsText(locale)` | Data or text for `llms.txt`. | | `getManifest()`, `getIndexNowKey()` | The web manifest and the IndexNow key. | ### Read a page and a list ```typescript const page = await atlas.getPage({ path: '/pricing', locale: 'en' }) if (!page) throw new Error('No such page') let cursor: string | undefined const posts = [] do { const batch = await atlas.getEntries({ type: 'post', locale: 'en', limit: 25, cursor }) posts.push(...batch.data) cursor = batch.next_cursor ?? undefined } while (cursor) ``` ### Errors and caching - A 404 returns `null`. Any other failing status throws an `AtlasApiError` with `status` and `url`. - The client keeps the ETag of every response and sends `If-None-Match` next time. A 304 returns the stored body, so repeated reads are cheap. - Use one client per brand and keep it alive. With a `previewToken` nothing is cached, because the API answers `private, no-store`. ```typescript import { AtlasApiError } from '@atlas/client' try { await atlas.getPage({ path: '/' }) } catch (error) { if (error instanceof AtlasApiError && error.status === 429) { // back off and try again } throw error } ``` ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # @atlas/nuxt Source: https://atlascms.id/docs/nuxt On this page ## @atlas/nuxt A Nuxt 4 layer that turns an AtlasCMS brand into a website: catch-all route, block renderer, SEO, redirects, preview and purge. - 2 min read ### Install and extend Install the four packages at the same version (`@atlas/nuxt`, `@atlas/blocks`, `@atlas/client`, `@atlas/tokens`) as described in the [client guide](/docs/sdk), then extend the layer. ```typescript export default defineNuxtConfig({ extends: ['@atlas/nuxt'], }) ``` ```bash NUXT_ATLAS_API_URL=https://api.atlascms.id NUXT_ATLAS_TOKEN=atl_read_... NUXT_ATLAS_PURGE_SECRET=the-webhook-secret ``` ### What you get - A catch-all route that asks the API for the page at the visited path and renders its blocks. A page file in your own app wins over the catch-all. - A block renderer and a block library, with your own brand blocks added next to it. - The theme tokens as CSS variables, header, footer and templates from the active theme. - SEO: titles, canonical links, Open Graph and generated share images, JSON-LD, sitemap, `robots.txt`, `llms.txt` and IndexNow. - Redirects from the API, category and tag archives, and a posts index. - Consent Mode v2 and consent-gated analytics, and a lead form block that posts through a rate limited server route. - Preview mode and the visual editor bridge for the dashboard. - The signed purge route described in [webhooks](/docs/webhooks). ### Configuration keys | Key (environment) | Purpose | | --- | --- | | `atlas.apiUrl` (`NUXT_ATLAS_API_URL`) | The API address. | | `atlas.token` (`NUXT_ATLAS_TOKEN`) | The read token of a single brand deployment. | | `atlas.purgeSecret` (`NUXT_ATLAS_PURGE_SECRET`) | The webhook secret of a single brand deployment. | | `atlas.sites` (`NUXT_ATLAS_SITES`) | Run several brands in one app: a JSON array of `{ host, brand, token, purgeSecret }`. A host outside the list is answered 421. | | `atlas.brandSlug` (`NUXT_ATLAS_BRAND_SLUG`) | The brand a preview token must belong to. | | `atlas.editorOrigins` (`NUXT_ATLAS_EDITOR_ORIGINS`) | Dashboard origins allowed to frame previews and drive the editor bridge. Empty turns the bridge off. | | `atlas.trustProxy` (`NUXT_ATLAS_TRUST_PROXY`) | Believe `X-Forwarded-For` and forward the visitor address to the API. | | `atlas.formsEndpoint` (`NUXT_ATLAS_FORMS_ENDPOINT`) | Override the lead target. Use `{key}` for the form key. | | `ogImage.secret` (`NUXT_OG_IMAGE_SECRET`) | Signs share image links. Required in production and the same on every instance. | | `public.atlas.locales`, `defaultLocale` | The languages of the site. | ### Composables Every composable uses an explicit fetch key, so server data survives hydration. If you wrap `useFetch` in your own code, give it an explicit key too. | Composable | Use | | --- | --- | | `useAtlasPage()` | The page entry a block is rendered on: `fields`, `terms`, `seo`, dates. Prefer it over copying text into block props. | | `useAtlasEntries(params)` | A cursor paged list of one entry type. | | `useAtlasMenu(key, locale)` | A navigation menu, or `null` when the brand has none. | | `useAtlasStrings({ locale, namespace })` | Interface texts with a per-key fallback to the default language. | | `useAtlasSettings()` | The public brand settings. | | `useAtlasTheme()` | Theme tokens and options. | ```typescript const { data: menu } = useAtlasMenu('footer', 'en') const { data: page } = await useAtlasEntries({ type: 'post', limit: 6, fields: undefined }) ``` ### Brand blocks Generic blocks ship in `@atlas/blocks`. A block only your brand needs lives in your site, with a prefix of its own, and is registered next to the generic ones. Describe each one in a definitions file and run the sync, so the dashboard offers it and the API validates it. ```bash atlas blocks:sync --schemas ./app/atlas/definitions.ts --custom-only --out brand-blocks.json ``` The same command line builds, checks, packs and pushes a theme: `atlas theme create`, `check`, `pack` and `push`. Pushing needs a `developer` token. ### Preview and the visual editor Set `NUXT_ATLAS_EDITOR_ORIGINS` to the dashboard origin. The dashboard then opens your site inside a frame with a short preview token. The layer checks that the token belongs to this brand before it sets its partitioned preview cookie, serves drafts with `no-store`, adds `noindex`, and lets the editor click a block to edit it in place. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Changelog Source: https://atlascms.id/changelog Changelog ## What’s new in ATLAS New features, improvements and fixes in the API, the dashboard, the SDK and the themes. We ship small and often. Read the docs Service status API Dashboard SDK and themes New Improved Fixed Breaking Show older releases --- # Frequently asked questions Source: https://atlascms.id/faq FAQ ## Answers, in plain words No question matches your search. Is ATLAS open source? No. ATLAS is proprietary software made by AtlasCMS. You can use it for free within the Free plan limits, but the source code is not shared, not on Free and not on Paid. Is the Free plan really free? Yes. There is no trial period and no credit card. You keep using it for free for as long as you stay within the Free plan limits. Can I use the Free plan for a business website? Yes. The Free plan is not only for testing. You can run one live brand on it for as long as you like, as long as you stay within its limits. What do I get when I pay? Paying removes the plan limits: more brands and seats, the review workflow with roles for every brand, and priority support. Can I host ATLAS on my own server? No. ATLAS is hosted by AtlasCMS, so you never have to run servers, updates or backups yourself. Can I take my content with me? Yes. You can export your content any time, in CSV or JSON. What do I need to get started? An AtlasCMS account and one brand. Create the brand in the admin, then connect your front end through the read API. Do I need a credit card to start? No. The Free plan has no trial period and needs no credit card. Which front ends can I use? Any front end that can call the read API. Nuxt sites can use the @atlas/client and @atlas/nuxt packages. Can I bring my existing content? Yes. You can import WordPress exports and CSV files, and a dry run shows what will change before anything is written. Do I lose earlier versions when I edit? No. Every change is kept as a new revision. Nothing is overwritten, so you can always go back. Can I schedule a page to go live later? Yes. You can publish or schedule a pinned revision, so the version you reviewed is the one that goes out. Can I roll back a mistake? Yes. Roll back to any earlier revision from the revision history. Who can publish? Editors write drafts and request a review. Publishers approve or send the page back, and admins manage the rest. Are drafts visible to the public? No. The public API returns published content only. Drafts are available through preview tokens, and those responses are never cached. How are brands kept apart? Every request is scoped to exactly one brand, so content, settings and media never mix between brands. Can each brand have its own look? Yes. Each brand has its own settings and theme. Where is media stored? Each brand has its own media storage, with folders and signed upload URLs. What roles are there? Editor, Publisher and Admin, set per brand. What does the read API return? Published content only, scoped to the brand of the token you use. Is the read API cached? Yes. Responses use ETag caching, so unchanged content answers with a 304. How do I preview a draft? Use a preview token. Preview responses are never cached. Can I deliver more than one language? Yes. Translations stay linked, and each locale is delivered on its own through the public API. Is there a sitemap? Yes. A sitemap-urls endpoint lists every published URL for your front end. How do I know a webhook is real? Webhooks are signed with HMAC-SHA256 in the X-Atlas-Signature header, so you can verify every event. Can AI publish on its own? No. Manual AI actions only ever create drafts. Autopost is off by default, an admin turns it on per brand, and every post is audited. How are API tokens handled? Tokens are shown once and never printed by the server. Can agents change my content? Agent tools are draft-only. They cannot publish, schedule, unpublish, upload or delete. ### Still have a question? Write to hello@atlascms.id. We reply within one working day. Contact us --- # Features Source: https://atlascms.id/features ## One API for many brands. One revision history. Content, review, translations, AI drafts, forms and delivery: every part of the content lifecycle, for every brand, in one place. Start free Read the docs Brands ### Every brand, cleanly separated. One API serves many brands, and every request is scoped to exactly one of them. - Multi-brand tenancy - Every request is scoped to its brand - Own settings and theme per brand - Separate media storage per brand Content ### Revisions you can trust. Every change is kept. Nothing is overwritten, so you can always go back. - Append-only revisions - Publish or schedule a pinned revision - Roll back to any earlier revision - Content types built from blocks Review ### Review before it goes live. A simple workflow so the right person signs off before anything is published. - Editors request a review - Publishers approve or send it back - Editor, Publisher and Admin roles Translations ### One entry, every language. Translations stay linked, and each locale is delivered on its own. - Translation groups link an entry’s translations - Locale-based delivery through the public API AI drafting ### AI drafts. People decide. AI helps you write faster, but a person stays in charge of what goes live. - Write, translate and suggest SEO fields - Draft image alt text for human review - Manual AI actions only ever create drafts - Autopost is off by default; an admin turns it on per brand, and every post is audited Forms & leads ### Turn visitors into leads. Collect leads straight from your sites and send them where your sales team works. - Public forms create leads - Forward leads to your CRM Delivery ### Fast reads. No leaked drafts. A read API built for front ends: cached, scoped, and published-only by default. - Read tokens return published content only - ETag caching with 304 responses - Preview tokens for drafts, never cached - A sitemap-urls endpoint SEO ### Search-ready by default. SEO lives with the content, so it is versioned and checked before you publish. - SEO fields stored per revision - Warnings before you publish - Redirects for moved URLs - IndexNow support Webhooks & media ### Signed events. Organised files. Trust every event you receive, and keep each brand’s files in their own place. - Webhooks signed with HMAC-SHA256 in the X-Atlas-Signature header - Media library with signed upload URLs - Folders and per-brand storage MCP & agents ### Let agents help, safely. Connect AI clients to your content without giving them the keys to publish. - A stdio MCP server reads the content model and entries, drafts included - Agent tools are draft-only: no publish, schedule, unpublish, upload or delete - Brand admins mint agent tokens with read scope and optional draft writes - Tokens are shown once and never printed by the server ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Talk to us - Free for one brand - No credit card - Upgrade anytime --- # Build the brand. Own the content. Source: https://atlascms.id/ v0.1.25 Database backups ## Build the brand. Own the content. ATLAS is a headless CMS for companies and agencies that run many brands. One admin for every brand, one fast read API for every website. Start free Read the docs - Free for one brand - No credit card - Hosted by ATLAS The ATLAS admin dashboard in dark mode dash.atlascms.id/yourbrand Powering brands, building possibilities. How it works ## Built for the whole team. Content teams run every brand from one admin. Developers read it all through one API. Pick your view. For content teams ### One admin, every brand Run all your brands from one place. Each one keeps its own pages, team and media. ### Write once, translate Write in Indonesian, add English when it’s ready. Visitors see their own language. ### Build pages from blocks Hero, product grid, FAQ, drag blocks into place. No developer needed. ### Review before it’s live Ask a teammate to check. Nothing goes live until someone approves it. ### Undo anything Every save is kept. Restore an older version in one click. ### Schedule it Pick a date and time. The page goes live on its own, even at night. dash.atlascms.id ATLAS - ID - EN ### ATLAS title page Page · Draft · edited 2 min ago ### Hero Banner, headline and button ### Product grid 12 products from Aksesoris ### FAQ 6 questions + Add block SD ### John Doe asked for a review 3 blocks changed Approve & publish - Version 13 · Restore v12 - Goes live Oct 20, 09:00 For developers ### Translations Add ?locale=id and get the Indonesian entry. No translation yet? It falls back to the default. ### Multi-brand tenancy The read token belongs to one brand. Ask for another brand’s record and you simply get 404. ### Caching built in Send the last ETag. If nothing changed, you get 304 and an empty body, fast, and cheap. ### Revisions Every publish pins one revision. A stale write answers 409 revision_changed, never mixed content. ### Review workflow Only reviewed, published revisions reach this API. Drafts and AI drafts never leak. ### Block-based pages Pages arrive as typed blocks. Map each one to a component in your front end. ### GET api.atlascms.id Also included - Signed webhooks - Roles per brand - Audit trail - Scheduling - Media library Quickstart ## Read content in a few lines A small, typed SDK for JavaScript and Nuxt. Your front ends read published content with a read token, so drafts never leak. - Typed client for Node, Nuxt and the browser - ETag and cache headers on every response - Brand comes from the token or the Host header - A stale revision answers 409, so you never show mixed content Read the quickstart Workflow ## From draft to live, safely Nothing goes live by accident. Each step is a clear state you can see in the admin. ### Draft Editors and AI write drafts. Drafts stay drafts. ### Review A publisher checks and approves the change. ### Publish Publishing pins one revision as live. ### Webhook ATLAS tells your site with a signed message. ### Live Your site revalidates only the pages that changed. Security ## Access you can audit Built for owners who run their own brands and for agencies who run their clients’. ### Isolated per brand Content, media and API tokens are kept apart for every brand. ### Roles per brand Editor, publisher and admin, set separately for each brand. ### Audit trail Every live change writes an audit row: who, what and when. Pricing ## Start free. Upgrade when your team grows. Start here - One brand to try the platform - Public read API and the admin - Community support Start free For teams - More brands and seats for teams - Review workflow and roles for every brand - Priority support Compare plans ATLAS is proprietary software. Free within the Free plan limits; paid plans remove the limits. Source code is not included in any plan. ## Questions, answered. Everything that usually comes up before a team picks a plan. See all questions Write to hello@atlascms.id Is ATLAS open source? No. ATLAS is proprietary software made by AtlasCMS. You can use it for free within the Free plan limits, but the source code is not shared, not on Free and not on Paid. Is the Free plan really free? Yes. There is no trial period and no credit card. You keep using it for free for as long as you stay within the Free plan limits. What happens when I need a second brand? Move to the Paid plan. Paying removes the limits, so you can add more brands and invite more of your team. Your content stays where it is. Do I get the source code on a paid plan? No. Paying unlocks every feature without limits, but the source code stays with AtlasCMS. ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Talk to us - Free for one brand - No credit card - Upgrade anytime --- # Quickstart Source: https://atlascms.id/docs On this page ## Quickstart Read your first page from ATLAS in about five minutes. You need a brand, a read token and a terminal. - 2 min read ### Free plan works for this guide Everything below runs on the Free plan with one brand. No credit card needed. ### Open your brand Sign in at [dash.atlascms.id](https://dash.atlascms.id) and open your brand. A brand holds the pages, posts, media, menus and settings of one website, and every API call is scoped to exactly one brand. Pick the languages you publish in under Settings, General. ### Create a read token Open Settings, Team, Access tokens and create a token of kind **Website (published content only)**. Copy it now: the dashboard shows the token once and only keeps a hash. Only an admin can create read tokens. ### Keep the token on the server A read token can read every published entry of the brand. Call ATLAS from your server or from Nuxt server routes, never from browser code. ### Fetch a page with curl Set your read token, then ask the API at `https://api.atlascms.id` for a page by its path. You only ever get published content. ```bash export ATLAS_TOKEN="atl_read_..." curl -s "https://api.atlascms.id/v1/public/page?path=/&locale=en" \ -H "Authorization: Bearer $ATLAS_TOKEN" ``` ### Read the answer The page comes back inside a `data` object, with its blocks, SEO, the links to its translations and the theme that renders it. A path the brand does not have answers 404. ```json { "data": { "id": "01m518vvbf8txh40wcvwhvtzyd", "type": "page", "locale": "en", "path": "/faq", "title": "Frequently asked questions", "seo": { "title": "FAQ | AtlasCMS", "description": "Answers about plans and the license." }, "fields": {}, "terms": [], "published_at": "2026-10-16T02:32:40.000000Z", "modified_at": "2026-10-21T14:42:20.000000Z", "blocks": [ { "id": "atlascms-faq-hero", "type": "atlascms-faq-hero", "v": 1, "props": {} } ], "translations": [], "fragments": {}, "route": { "kind": "entry", "template": "page", "candidates": ["page-faq", "page", "singular", "index"] }, "theme": { "slug": "atlascms", "version": "0.1.0" }, "fallback": false } } ``` ### Read it with the SDK The client is a typed wrapper around the same API. It sends your token, revalidates with ETags and returns `null` for a 404. The packages are published privately to GitHub Packages under the `@lifefuturedev` scope; write to [hello@atlascms.id](mailto:hello@atlascms.id) for access. ```typescript import { createAtlasClient } from '@atlas/client' const atlas = createAtlasClient({ baseUrl: 'https://api.atlascms.id', token: process.env.ATLAS_TOKEN!, }) const page = await atlas.getPage({ path: '/', locale: 'en' }) console.log(page?.title, page?.blocks?.length) ``` ### Or render it with Nuxt The Nuxt layer adds a catch-all route that asks the API for the page at the visited path and renders its blocks with your theme. Extend the layer and set two environment variables. ```typescript export default defineNuxtConfig({ extends: ['@atlas/nuxt'], }) ``` ### Set the environment Put `NUXT_ATLAS_API_URL` and `NUXT_ATLAS_TOKEN` in `.env`. The [Nuxt layer guide](/docs/nuxt) lists every key. ### Refresh the site when you publish Add a webhook under Settings, Publishing, Hooks that points at your site. ATLAS signs every message, so the site can check the signature before it clears its cache. The [webhooks guide](/docs/webhooks) has the details and a verification example. ### Need help? Write to the team. Paid plans get priority support. Contact us → --- # Atlas Shield: spam protection built into every Atlas form Source: https://atlascms.id/blog/atlas-shield-spam-protection-for-forms Every form on an Atlas site can now defend itself. Atlas Shield is spam protection built into the Atlas API: bots that post straight to your forms are turned away, suspicious leads are held for review instead of reaching your CRM, and your ad platforms stop learning from fake conversions. ## Why we built it A lead form is a public door. Until now the only lock was a per address rate limit, which a bot that rotates addresses walks straight past. Every fake lead was stored, sent to the CRM, reported to Meta and TikTok as a Lead conversion and announced to your team. The cost is not the inbox: it is the ad algorithm optimising for bots. ## How Atlas Shield works - A signed, single use challenge. Before a form is sent, the browser asks Atlas for a challenge token. It is signed for your brand and that one form, it expires, and it works once. A bot that posts straight to the API has none, and a copied token cannot be replayed. - Invisible proof of work. The browser solves a small puzzle while the visitor types. People never notice it; a bot pays for every single send. - A time trap. A form filled and sent in under a few seconds is not a person. - Content rules. Too many links, HTML in a field or the same message sent again within minutes are flagged. - Captcha when you want it. Turn on Cloudflare Turnstile or hCaptcha for any form. Turnstile managed, non-interactive and invisible widgets are all supported, and so is invisible hCaptcha. Atlas checks every token itself, including that it was solved on your own domain for that form. ## Spam is held, never lost A lead that looks suspicious is not thrown away. It is stored as spam with the reasons next to it, kept out of the CRM, out of conversion tracking and out of notifications. If a real person was caught, one click on Not spam sends the lead on as if nothing happened. If a captcha provider has an outage, leads are held for review too, so you never lose a customer to someone else's downtime. ## You decide how strict Atlas Shield has three modes: Off, Monitor (every lead is taken and the reasons are only noted, ideal while you roll it out) and Enforce. Set a default for the whole brand in Settings, Forms, give a single form its own settings, or change them right on the form block in the visual builder. The minimum time, the proof of work level, the links allowed and the duplicate window are all yours to tune. ## Do I need to do anything? Sites that use the Atlas form block answer the challenge on their own once they run the latest layer. Brands that already take leads start in Monitor, so nothing breaks while you look at the reasons; switch to Enforce when you are happy. For a captcha, add your Turnstile or hCaptcha keys under Integrations, Security and pick it for your forms. The details are in the docs. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Dashboard 0.1.22: the notification bell counts the brand you are on Source: https://atlascms.id/blog/dashboard-0-1-22-notification-count-per-brand Dashboard 0.1.22 is out with one fix that anyone working across several brands will notice: the notification bell in the top bar now counts the brand you are looking at. ## What was wrong The bell showed the number of unread notifications you had in all your brands, while the panel it opens lists the active brand only. On a quiet brand you could see a red 10 and then an empty panel, because those ten notifications belonged to another brand. No notification was shown in the wrong brand: notifications belong to you, and the list was always scoped to the brand on screen. Only the number on the bell was off. ## What changed - The bell and its screen reader label count the active brand, the same scope as the panel and its Unread tab. - The Mark all as read button on the Notifications page follows that count too, because it only marks the active brand. - When you switch brands, a count that arrives late for the brand you left is ignored, so it can never land on the new one. ## Do I need to do anything? No. The dashboard updates itself; reload dash.atlascms.id if a tab has been open for a while. The full list of changes is in the changelog. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # llms.txt and llms-full.txt for every brand Source: https://atlascms.id/blog/llms-txt-for-every-brand AI assistants read websites differently from people. They want the text, in order, without menus, cookie banners and scripts in the way. The llms.txt convention gives them an index of a site, and llms-full.txt gives them the full text in one file. Starting with API 0.1.31 and @atlas/nuxt 0.1.2, every brand on ATLAS serves both files. You do not write a line of code for it. ## Built in the API, not in each site A company that runs ten brands should not maintain ten copies of the same feature. So the work happens once, in the delivery API: GET /v1/public/llms?format=full walks every published page and post of the brand and turns it into Markdown. Entries marked noindex stay out. The layer then serves /llms.txt and /llms-full.txt for any site built on it. Upgrade the layer and the routes are there. ## Theme neutral by design Brands use different themes, and themes define their own blocks. Instead of teaching the API every block, the renderer reads the shape of the props: headings become headings, lists become lists, columns and rows become tables, source code becomes a code fence and rich text is converted to Markdown. Everything a model does not need is left out: links, media, ids, form fields, validation messages and other machine values. What remains is the copy your editors wrote. ## Limits - Up to 500 entries per locale. - The file stops at 2 MB, so a very large brand gets its newest content first. - A site that ships its own /llms.txt route keeps it: a site route wins over the layer. Try it on this site: /llms.txt and /llms-full.txt. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Node.js October 2026 security releases: what to update Source: https://atlascms.id/blog/nodejs-october-2026-security-releases On 21 October 2026 the Node.js project shipped security releases for every supported line: Node.js 26.12.1 (Current), 24.22.1 (LTS) and 22.23.4 (LTS). ## The high severity fixes - CVE-2026-111497: an out of bounds write in the Brotli decoder that permits remote code execution. - CVE-2026-106069 and CVE-2026-105483: ways around the TLS certificate hostname check when a host loosely resolves to an IP address, or when an IP is written in fullwidth digits. - CVE-2026-105482: dns.resolve truncates names that contain a NUL character. ## Also in this release - crypto.randomUUID() could repeat the same values in every process of a single executable application (CVE-2026-106074, medium). - Permission Model fixes for filesystem grants and fs.realpathSync (low). - Dependency updates for c-ares, Undici, the CA certificates (the eMudhra intermediate CA compromise), OpenSSL on 24.x and 22.x and SQLite on 22.x. ## What to do - Update to 26.12.1, 24.22.1 or 22.23.4, whichever line you run. - Rebuild container images and restart long running processes so the new binary is the one serving traffic. - On a hosting panel, pick the new version in the Node.js app settings and restart the app. ## What it means for ATLAS sites Sites built on @atlas/nuxt run on Node.js 22 or newer, so they should move to one of the patched versions. Details are in the Node.js announcement. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # PHP fixes two critical remote code execution bugs: update now Source: https://atlascms.id/blog/php-critical-rce-update-now On 21 October 2026 the PHP project released PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. They fix two critical vulnerabilities that can allow unauthenticated remote code execution, and the PHP team asks every user to update immediately. ## Who is at risk Every PHP installation that serves web requests: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. The PHP team rates mod_php highest, with a demonstrated exploit where a request triggers the attack before any application code runs. ## The two vulnerabilities - CVE-2026-109618, RCE via OOM corruption. Out of memory handling can interrupt PHP while internal request data is only partly initialised, and request cleanup may then treat that data as valid. - CVE-2026-109619, RCE via stale global state. Request owned internal state can survive after its memory is freed, and a later request on the same worker may use it before normal initialisation resets it. ## What to do - Update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35. Linux distributions and LTS vendors received the patches in advance, so check your vendor's packages too. - Restart every PHP worker after the update, including long running ones such as FrankenPHP or Octane. - On PHP 8.1 or older, upgrade to a supported version. The PHP team published minimal source backports for 7.4, 8.0 and 8.1 for people who build from source. ## If you cannot update today The advisory lists measures that reduce exposure but do not make an installation safe: set session.upload_progress.enabled=0 in php.ini or the FPM pool and restart every worker, on PHP 8.5 set fatal_error_backtraces=0, and keep post_max_size low (8M without uploads, 32M with them). Only the update is a complete fix. ## What it means for ATLAS The ATLAS API runs on PHP 8.5, so a self hosted ATLAS needs PHP 8.5.12 or later. Read the full advisory on php.net. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # atlascms.id is live, and it runs on ATLAS Source: https://atlascms.id/blog/atlascms-id-is-live The best test of a CMS is to build with it. So atlascms.id is not a separate marketing stack: it is the atlascms brand on ATLAS, rendered by the same Nuxt layer our customers use. ## Nothing product facing is hardcoded Every page is a list of blocks stored in ATLAS. The header and footer read their menus from the API, and the copyright line is a translatable string. Editing the site means editing content in the dashboard, not opening a pull request. ## Docs and changelog stay in step with the code - Docs live next to the API source and sync to the site when they change, so the reference never drifts from the code. - The changelog is written by our release tooling: each release posts an entry, and the page lists them newest first. ## Two languages from day one The site serves English and Indonesian. Each page is a translation group, and the layer sets the right hreflang links and canonical URL for every version. If you want to see how it fits together, the Nuxt guide walks through the same setup. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # How ATLAS keeps brands apart Source: https://atlascms.id/blog/how-atlas-keeps-brands-apart Serving content for many independent brands often forces a choice between separate CMS installs per brand or a fragile custom tenancy layer. ATLAS builds the isolation into the platform instead. ## Every query is scoped to a brand Each request to the manage and delivery APIs runs inside an explicit brand scope. A brand owned record outside that scope does not exist as far as the request is concerned: a foreign id answers 404, and code that forgets to set a brand fails closed instead of reading everything. ## Revisions are append only Edits never overwrite live content. Every save is a new revision, so editors can iterate, ask for a review and preview a draft with a short lived token before a publisher puts it live. Rolling back is choosing an older revision. ## Fast delivery with ETags Public responses carry an ETag tied to the brand's content version. A site asking again for an unchanged page gets a 304 straight from cache, and publishing bumps the version and sends a signed purge webhook so the next request is fresh. The security docs go deeper into roles, tokens and outbound calls. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Scheduled publishing across time zones Source: https://atlascms.id/blog/scheduled-publishing-across-time-zones A launch at 09:00 in Jakarta is 02:00 in London and 21:00 the day before in New York. ATLAS stores every schedule as an exact moment in time, so the page goes live once, at the right instant, for everyone. ## Schedule from the dashboard - Open the entry and save your draft. - Choose Schedule and pick the date and time. - Confirm. The entry shows its scheduled time until it goes live, and you can cancel the schedule at any point before that. Scheduling changes live output, so it needs the publisher role or higher. Editors can prepare the draft and ask for a review. ## Schedule through the API POST /v1/manage/entries/{entry}/schedule takes an at date that must be in the future. Send it in ISO 8601 with an offset, so there is no doubt which time zone you mean: { "at": "2026-11-01T09:00:00+07:00" }DELETE /v1/manage/entries/{entry}/schedule cancels it. Pass revision_id to pin the revision you reviewed: if someone saves a newer draft before you schedule, the API answers 409 revision_changed instead of quietly publishing something you did not check. ## What happens at that moment The scheduler checks every minute and publishes due entries through the normal publish path: a new live revision, an audit row and a purge webhook so your site shows the page straight away. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Preview tokens that expire on their own Source: https://atlascms.id/blog/preview-tokens-that-expire-on-their-own Sometimes a draft needs eyes outside the dashboard: a client, a lawyer, a colleague on their phone. Publishing it early is not an option, and a password protected staging site is one more thing to run. ATLAS uses preview tokens instead. ## What a preview token is - A token of kind preview that always expires. Any member may create one for 1 to 120 minutes. - It sees the latest draft of every entry, not only what is live. - Its responses are never cached and the page is marked noindex, so a draft cannot leak into a CDN or a search engine. - When the time is up it simply stops working. There is nothing to revoke. ## Create one Any member of the brand may create a preview token, editors included, because it shows drafts but changes nothing: POST /v1/manage/api-tokens { "name": "Client review", "kind": "preview", "expires_in_minutes": 30 }Other token kinds, such as the read token a site uses in production, stay admin only. ## Open the draft on the real site Sites built on @atlas/nuxt ship a preview route. It stores the token in an httpOnly cookie, so the reviewer browses the real site, with the real theme, and sees the draft wherever it appears. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Webhooks with signed purge requests Source: https://atlascms.id/blog/webhooks-with-signed-purge-requests Fast sites cache. The hard part is knowing when to stop serving the cached copy. ATLAS sends a webhook the moment live content changes, so your site can purge exactly when it needs to and keep its cache the rest of the time. ## What gets sent Every delivery is a JSON POST with four headers: - X-Atlas-Event: what happened, for example a publish or a menu update. - X-Atlas-Delivery: a unique id, useful for logs and for ignoring repeats. - X-Atlas-Timestamp: Unix seconds when the request was signed. - X-Atlas-Signature: sha256= followed by an HMAC of the timestamp and the body. ## Check the signature The signature is HMAC SHA-256 over timestamp.body, keyed with the webhook secret. In Node.js: import { createHmac, timingSafeEqual } from 'node:crypto' function isFromAtlas(body, timestamp, signature, secret) { const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex') const a = Buffer.from(expected), b = Buffer.from(signature) return a.length === b.length && timingSafeEqual(a, b) }Use the raw body exactly as received, compare in constant time, and reject old timestamps so a captured request cannot be replayed later. ## Sites on the layer do this for you @atlas/nuxt includes a purge endpoint that checks the signature and clears its cached API answers. Every delivery and its result is also listed in the dashboard, so a failing endpoint is easy to spot. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # One theme, many brands Source: https://atlascms.id/blog/one-theme-many-brands Agencies and groups often run brands that share a structure but not a look. In ATLAS a theme is the shared structure, and everything that makes a brand feel like itself stays with the brand. ## What lives in the theme - Templates such as the front page, a page, a single post and the 404 page, each a list of blocks. - Parts such as the header and footer. - Blocks with their schema, so the dashboard knows which fields to show. ## What lives with each brand - Design tokens: colours, fonts and radius, with a dark mode set. - Theme options, saved as a draft and published when ready. - Template overrides: a brand can edit a template without touching the theme package, and reset it to the package version later. ## Try before you switch Switching themes is a big change, so it comes with two safety nets. A switch check lists what would not carry over, such as menus nested deeper than the new theme allows. A preview draft renders the candidate theme for preview tokens only, so the team can walk the real site in the new theme while visitors keep the old one. When the brand is happy, it activates the theme. One theme update then reaches every brand that runs it. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Importing a WordPress site in minutes Source: https://atlascms.id/blog/importing-a-wordpress-site-in-minutes Moving off WordPress usually means weeks of copy and paste. ATLAS reads the standard WordPress export file and brings the content over for you. ## 1. Export from WordPress In WordPress, go to Tools, Export and download the export file. ATLAS reads WXR versions 1.0 to 1.2. ## 2. Run a dry run In the ATLAS dashboard, open Tools, Import, choose WordPress and upload the file with dry run on. The report shows what would be created and updated, with nothing written yet. Fix anything surprising and run it again. ## 3. Import for real Pick the options and start the import: - Status: keep the WordPress status, or bring everything in as drafts or as published. Private posts always stay drafts, and a member without the publisher role never publishes. - Download images: attachments and images used in the content are downloaded into the brand's media library, keep their alt text, and old upload URLs redirect to the new files. Links between your own pages become relative paths, so they keep working on the new domain. ## Run it again whenever you like Entries remember the WordPress post they came from. A second import updates posts that changed and leaves the rest alone, which makes a gradual move easy: keep writing in WordPress until launch day, then import one last time. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Translation groups explained Source: https://atlascms.id/blog/translation-groups-explained Some CMSs store all languages inside one record. ATLAS gives every language version its own entry and links them in a translation group. It sounds like a small detail, but it shapes the whole editing flow. ## Why separate entries - Each language has its own draft, review, schedule and revision history. The Indonesian page can go live tomorrow while the English one is still in review. - Each language can have its own path, for example /about and /id/tentang, while ATLAS still knows they are the same page. ## What the group gives you - Correct hreflang. The page payload lists its translations, and the layer writes the hreflang links and the canonical URL for every version. - A language switcher that lands on the same page in the other language, not on the home page. - Outdated translation warnings when the source changes after a translation was written. - Grouped lists in the dashboard: one row per page with every language beside it, plus a filter for pages still missing a language. ## Add a translation Open the entry, choose the language and start the translation. It joins the group automatically. Settings that point at a page, such as the front page and the posts page, point at the group, so they follow every language. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Faster lists with light mode entries Source: https://atlascms.id/blog/faster-lists-with-light-mode-entries A list page shows a title, a date, an excerpt and an image per post. Sending the full body of twenty posts to draw that wastes bandwidth and time. The delivery API has a light mode for exactly this. ## Ask for light entries GET /v1/public/entries?type=post&locale=en&fields=light&count=1- fields=light drops the blocks and keeps only what a card needs. The SEO data is reduced to the social image, as {media_id, url, thumbnail_url}, and posts carry author_name. - count=1 adds the total number of matches, so you can show page numbers. ## Page through it Lists are cursor paged: pass next_cursor from one answer to get the next. For a classic blog with numbered pages, the layer turns /blog/page/3 into the right cursor for you, up to the configured page limit. ## The layer already does it useAtlasEntries() in @atlas/nuxt asks for light entries by default and adds the count on the first page. Pass full: true only when a list really needs the blocks. This blog uses it: the index you came from loads light entries. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Roles for editors, publishers and admins Source: https://atlascms.id/blog/roles-for-editors-publishers-and-admins Every member of a brand has one of three roles. The line between them follows one question: does this action change what visitors see? ## Editor Writes and edits drafts, uploads media and asks for a review. An editor can also create a short lived preview token to show a draft to someone. Nothing an editor does reaches the live site on its own. ## Publisher Everything an editor can do, plus anything that changes live output: publish, schedule, roll back, unpublish, approve reviews and move published pages. Publishers also handle form submissions and leads. ## Admin Everything a publisher can do, plus the brand itself: settings, members, API tokens, integrations and secrets, and imports. Secrets are write only: once saved, even an admin cannot read them back. ## Built in, not bolted on - Every live change writes an audit row: who, what and when. - Roles are per brand, so someone can be an admin on one brand and an editor on another. - A content type can require a minimum role, for example only publishers may touch the pricing pages. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Media storage per brand Source: https://atlascms.id/blog/media-storage-per-brand By default every brand stores media on the shared ATLAS disk. When a brand needs its files in its own account, for billing, data location or an existing CDN, an admin can point it at its own storage. ## Supported targets - Amazon S3 - Cloudflare R2 - Backblaze B2 - Any other S3 compatible store, with a custom endpoint - The local server disk ## Set it up - Create a bucket and an access key that can write to it. - In the dashboard, open the brand's media storage settings and choose the provider. - Enter the endpoint, region, bucket and keys, then run Test. ATLAS writes and removes a tiny probe file to prove it can write, without saving anything yet. - Save. New uploads go to the new storage. ## Nothing breaks when you switch Switching provider does not move or delete existing files. Earlier uploads stay where they were and keep working, and new ones go to the new place. The access keys are write only: the settings screen shows whether credentials are set, never the keys themselves. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime --- # Reading settings for any front page Source: https://atlascms.id/blog/reading-settings-for-any-front-page Every brand decides how its site opens and where its blog lives. Reading settings hold those choices, so they change in the dashboard instead of in code. ## The four settings - Home page shows: a static page, or your latest posts. - Front page: the page served at the root when the home page is static. - Posts page: the page that lists the blog, for example Blog at /blog. - Posts per page: 1 to 100, 25 by default. The front page and the posts page point at a translation group, not one entry, so every language follows the same choice and each language keeps its own path. ## What the site does with them - The posts page shows its own blocks, such as a header, followed by the list of posts. - Numbered pages work out of the box: /blog/page/2, /blog/page/3 and so on, with the right canonical and previous and next links. - Category and tag archives borrow the posts page layout. - The sitemap shows the posts page as updated whenever a new post goes live. ## This blog is an example On atlascms.id the front page is a static page, the posts page is Blog, and each page lists nine posts as a three by three grid. Changing any of that is one setting away. Keep reading ## More from the blog ## Run every brand from one place. Start with one brand for free. Bring your team when you are ready. npm install @atlas/client @atlas/nuxt Start free Read the docs - Free for one brand - No credit card - Upgrade anytime