Organization restore
Purpose: restore a CMDash tenant export archive into a workspace — a clean recovery after a failure, or a migration from another CMDash deployment.
On the hosted (SaaS) service this page's upload flow is refused: both
POST /data-importandPOST /data-import/preflightanswer 403DATA_IMPORT_SELF_HOSTED_ONLYbefore doing anything, because an arbitrary archive can name anything and a restore writes into a workspace on infrastructure every organization shares. What the hosted service does restore is its own sealed export, into the same or a new workspace — see Restore a hosted export below. For anything else, contact support to migrate the data.
The page is /data-import, next to Data Export in the Admin section. It
was previously labelled "Data Import"; the URL is unchanged — only
the label and title moved, so this remains the OWNER-only full-tenant restore
tool, not the CSV importer (that's the upload dialog on the Risk register
and its own import page).
Before you start
| Requirement | Value |
|---|---|
| Role | Owner only |
| Target workspace | Must be empty — an import into a workspace that already holds data is refused |
| File | The JSON archive from a Data Export job (not the CSV bundle) |
| Members | Invite the people you want before importing — an archive never adds members (see below) |
An import writes to essentially every table in the target workspace. That is why it sits one level above the Owner-only bar on requesting an export: an export is a tenant-wide read, an import is a tenant-wide write.
How to restore
- Create (or pick) an empty workspace and sign in as its Owner.
- Open Organization restore from the Admin section.
- Choose the JSON archive from a Data Export job. Uploading it runs a dry run immediately — a server-side preflight check that writes nothing.
- Read the dry-run summary. It shows the source workspace, when the export was taken, how many tables and rows would be restored, per-table detail, and — most importantly — every reason the import would be refused, all at once rather than one per attempt.
- If the dry run is clean, type
IMPORTand select Restore into this workspace. - The result names how many rows landed, across how many tables.
If the apply fails part-way, the importer rolls back the rows it wrote and records the reason on the import job. It never leaves a half-restored workspace behind.
What is never restored
Billing records stay with the workspace they belong to. The export includes them, but a restore skips them, and the dry run lists each one as skipped: the plan and its entitlements, the Stripe subscription and its add-ons, enterprise contracts and their invoices, the AWS Marketplace link and its metering, AI credits, metered usage billing, feature trials, referrals, design-partner enrolment, and partner statements. The target workspace keeps its own plan, add-ons and contracts.
Access to the workspace is never restored. An archive carries business data only; who can reach the workspace is decided by its Owner, not by a file. A restore skips memberships, invitations, join requests, delegated-admin grants, SCIM groups, SAML/LDAP configuration, MFA and password policies, sessions and sign-in codes, authenticators, API keys, share links, document room grants, notification channels (their webhook URLs are secrets), and every stored integration or destination credential. The dry run lists each as skipped with its row count, and so does the result. Assessor and vendor-respondent grants are restored revoked (the engagements and findings that reference them stay intact), exposure domains are restored removed (add a domain again to prove it), and due-diligence sessions are restored expired. Nothing restored sends e-mail until you turn it on here: Trust Center subscribers arrive unsubscribed, scheduled reports paused, executive briefings switched off, and a briefing that was awaiting approval arrives as not sent.
People in the archive are matched to your members. Every column that names a person (an owner, an assignee, who created or approved something) is kept only when it names someone who is already a member of this workspace: the same account, or an archive member whose e-mail address matches one of your members. Anyone else is cleared; where a record must name someone (who created a vendor, who raised a finding) it names you, the person running the restore. Records that are one person's own act or state — an attestation, an affirmation, a sign-off, a signature, a decision, a personal notification — are restored only when that person is you, the person running the restore: an archive file is not signed, so it cannot vouch for anyone else's act. The dry run says how many of each will happen. Invite people before you restore to keep them on ordinary records (owners, assignees).
Credentials inside restored records are cleared too: a policy's public access code, a Trust Center custom domain and its proof, a questionnaire tracking link, and a pending deletion's confirmation token.
Why an import is refused
| Refusal | What to do |
|---|---|
| "This workspace already contains data" | Restore into a freshly created workspace. There is no merge mode — see below. |
| Unsupported archive format version | The archive came from a much newer or much older CMDash. Formats 2.0 and 2.1 are accepted. |
| "not a CMDash tenant export archive" | You uploaded the CSV bundle, or a single table's CSV. Upload the JSON archive. |
Why there is no merge mode. Two independently generated workspaces have independent id spaces. Merging them would need a documented answer for every collision — a duplicate risk id, a duplicate policy slug — and there is no principled one. Refusing a non-empty target is always safe to retry against a clean workspace; a half-merged workspace is not.
What is not restored: files
A restore copies records, not files. The JSON archive names the files your records point at (uploaded evidence, a Document Room item, a contract's purchase order, a deletion certificate, a generated report), but the files themselves stay where they were stored, in the workspace the export came from.
A restored record keeps a file reference only when that file lies in this workspace's own storage. Any other reference is removed and the record is still restored: an evidence item keeps a note of each removed file (for an upload, its name, size and SHA-256), and a Document Room item that lost its file is switched off until you point it at a file again. An evidence item that was collected, reviewed or approved but lost every file goes back to Pending, and its note records the status it had: an approval whose file did not come across is not counted as one. The dry run tells you how many references would be removed, and where, before you restore anything.
To bring the files across, upload them again in the restored workspace. The Data Export's uploaded-files ZIP holds the source workspace's files; if it reached its size limit, its manifest lists every file it left out.
What is not restored: configuration
An export never contains a live secret, so an import can never restore one. The following are absent from every archive and must be set up again by hand in the restored workspace:
- SSO / SAML — the IdP signing certificate and the SCIM bearer token
- LDAP directory sync — the bind password
- Integration inbound webhook secrets
- Outbound webhook signing secrets
- API keys (the keys themselves; their metadata is recorded — see below)
The archive does carry a configuration bundle — a redacted record of all of
the above plus notification channels and rules, framework scope, and custom
field definitions. It is documented in
Retention & Export. It is a
record, not a restore artifact: the importer reads only the archive's
models section and ignores the configuration bundle entirely.
What a config restore would take
Re-applying configuration automatically is not implemented, and would need three things that do not exist today:
- A secret channel. Every configuration object that matters is only useful with its secret (bind password, signing secret, API key). Restoring config therefore means either re-entering each secret by hand — which is what this page tells you to do now — or exporting secrets, which would turn an archive into a credential dump.
- An idempotent per-object apply path. Each of these objects is owned by a
different module with its own validation and side effects (an integration
connection schedules a sync; a SAML config affects login). A generic
createManyover them, the way the data importer works, would bypass all of it. - A conflict policy for a NON-empty target. Config restore is most useful into a workspace that already exists, which is exactly the case the data importer refuses. That needs its own design.
Until then: restore the data with this page, then rebuild integrations and SSO from the configuration bundle, which lists every object you had and every setting except the secrets.
Restore a hosted export
An export taken on the hosted service (format 2.4 or later) carries a seal at the end of its
JSON archive: a check value only this service can produce, tied to your organization, the export
job and the exact content. The hosted service restores such an export through
/api/data-import/reimport (Owner only):
| Requirement | Value |
|---|---|
| Archive | The JSON archive of a Data Export taken on this service, unchanged |
| Target | The organization it came from, or a new organization created after the export |
| You | An Owner of the target with a verified e-mail address. For a new organization, also an owner of the exported one: a current Owner while it is still active; once it is closed, the same account that was an Owner at export time, or the same e-mail address proven with a one-time code sent to it |
| Target state | Active, not being closed, in the same data region, and empty apart from the records listed under What a new workspace may already hold |
| Plan | Room for the export's frameworks in scope and integrations |
| Size | Up to 20 000 rows per table, 250 000 in total, 5 MB per request |
| When | Within 365 days of the export (the service's EXPORT_SEAL_TTL_DAYS) |
- Dry run —
POST /api/data-import/reimport/dry-runwith{ "archive": … }. It writes no workspace data and returns every reason a restore would be refused (blockers, each with a code), the warnings, the per-table detail, the tables it will not write (droppedTables), how your ownership was shown (ownerProof) and — only when nothing blocks — aconfirmationvalid for 15 minutes. WhenownerProofisEMAIL_CODE(the exported workspace is closed and only your e-mail address matches its owners), the dry run also e-mails a one-time restore code to that address (ownerCodeSentToshows it masked). The code expires in 15 minutes, works once, allows five tries, and at most five codes are sent per hour; each new dry run replaces the previous code. If this service cannot send e-mail, the dry run refuses (REIMPORT_OWNER_EMAIL_UNAVAILABLE) rather than skip the proof. - Confirm — re-enter your password or authenticator code (
POST /api/auth/reauth), thenPOST /api/data-import/reimportwith{ "archive": …, "confirmation": …, "idempotencyKey": … }(plus"ownerCode": …when the dry run sent one) and theX-Fresh-Authheader. Every check runs again. Sending the sameidempotencyKeyagain returns the recorded result instead of importing twice. - Check —
GET /api/data-import/reimportlists your restore attempts; the audit log recordsdata_import.reimport_startedanddata_import.reimport_completed(or…_failed).
In the app. On the hosted service the Restore a CMDash archive page (/data-import) carries this
flow: choose the JSON archive, read the dry run (blockers, warnings, tables that will not be written),
type RESTORE, enter the one-time code if the dry run mailed one, and confirm your identity when asked.
A retry after a dropped connection reuses the same idempotency key, so it replays the recorded attempt
instead of restoring twice.
What is restored and what is not is exactly as for any restore (above): business data only — never members, sign-in methods, API keys, stored credentials or billing records — and file references only inside this workspace's own storage.
What a new workspace may already hold
A new workspace is never literally empty: signing up, signing in and re-authenticating write records of their own. A restore accepts a workspace whose only records are these, and refuses any other data:
| Records the workspace may already hold | What the restore does with the export's records of the same kind |
|---|---|
Audit log (audit_logs) | Adds the exported entries beside the workspace's own |
Notifications (notifications) | Adds the exported notifications |
Data export history (data_export_jobs) | Adds the exported job history (the files themselves are not restored) |
Import history (tenant_import_jobs) | Adds the exported history; the restore records its own job too |
API usage log (api_usage_logs) | Adds the exported usage entries |
Growth milestones (growth_events) | Not restored — this workspace keeps its own |
Trial e-mails sent (trial_lifecycle_emails) | Not restored — this workspace keeps its own |
Plan-gate hits (upgrade_gate_hits) | Not restored — this workspace keeps its own |
Onboarding progress (onboarding_progress) | Not restored — this workspace keeps its own |
The dry run lists the four "not restored" kinds under droppedTables when the export holds any.
One export restores one workspace. Once an export has been restored, it is refused everywhere
else (REIMPORT_SEAL_ALREADY_USED); export again from the restored workspace if you need another
copy. A failed restore is rolled back; when the rollback removed everything, the export is free to
be tried again. When anything it wrote is left behind, the export stays claimed so a retry cannot
duplicate it — contact support. A restore whose process stopped is released after 24 hours.
How long an export can be restored. The service keeps its record of an export for 365 days (or 30 days after it is withdrawn), then deletes it; after that the export cannot be restored here.
| Code | What it means |
|---|---|
REIMPORT_SEAL_MISSING | The archive has no seal: not a hosted export, taken before format 2.4, or the CSV bundle |
REIMPORT_SEAL_INVALID | The archive was changed after the export |
REIMPORT_SEAL_OTHER_DEPLOYMENT | The export came from a different CMDash deployment |
REIMPORT_SEAL_UNKNOWN | This service has no live record of the export (withdrawn, or older than 365 days) |
REIMPORT_TARGET_NOT_ELIGIBLE | This workspace existed before the export, or is not active / is being closed |
REIMPORT_OWNER_NOT_VERIFIED | Your e-mail address is not verified, or you were not an owner of the exported workspace |
REIMPORT_REGION_MISMATCH | The export and this workspace are held in different regions |
REIMPORT_PLAN_LIMIT | The restore would exceed this workspace's plan |
REIMPORT_IMPORT_BLOCKED | The workspace already holds data, or another restore check failed (see blockers) |
REIMPORT_CONFIRMATION_INVALID | Run the dry run again: the confirmation is missing or expired, or the idempotency key is malformed |
REIMPORT_IN_PROGRESS | A restore into this workspace is already running |
REIMPORT_OWNER_CODE_REQUIRED | The restore needs the code e-mailed by the dry run (none sent, expired, or replaced) |
REIMPORT_OWNER_CODE_INVALID | The restore code is wrong or already used |
REIMPORT_OWNER_EMAIL_UNAVAILABLE | Your ownership must be proven by e-mail and the service cannot send e-mail right now |
REIMPORT_OWNER_CODE_RATE_LIMITED | Too many restore codes in the last hour |
The full list is in the error catalogue.