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.

TaskWho
WordPress and CSV imports, redirects CSV import, exportspublisher or admin
Website crawl import, database importers, theme bundleadmin

Import a WordPress export or a CSV

GET/v1/manage/tools/imports/sources

Lists the sources you may use: wordpress (a .xml export), csv, and for admins url.

POST/v1/manage/tools/imports

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.

terminalbash
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:

terminalbash
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.

OptionDescription
localeLanguage of the imported content. Must be one of the brand's languages.
statuskeep, draft or published. Private posts always stay drafts, and a member who cannot publish never publishes.
type_mapWordPress only: { "post": "post", "page": "page" }, each post, page or skip.
typeCSV only: post or page.
category_map, tag_map{ "old-slug": "new-slug" }. A null value drops the term.
download_imagesCopy images into the media library and redirect the old upload addresses.
rewrite_linksTurn links to the old site into relative links (default on).
path_prefixCSV 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/v1/manage/tools/redirects/import

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.

CommandWhat 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.
terminalbash
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.

requestjson
{ "kind": "csv", "filters": { "type": "post", "locale": "en", "status": ["published"], "from": "2026-01-01" } }
KindContent
jsonEntries with their latest and published revisions, terms and translation group.
csvThe import columns, so it round trips with the CSV import.
wxrWordPress compatible, posts and pages only.
mediaid, filename, mime, size, width, height, alt, url, created_at.
redirectsfrom, 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:

downloadjson
{ "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.

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.

responsejson
{ "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.

Was this page helpful?

Suggest a change