Importing and Exporting Organizations

This guide shows you how to export an organization — or one tenant of it — to a compressed file and import that file on another Enterprise Stack installation. Typical uses are moving an organization between data centers and restoring entries deleted by mistake.

Import and export are behind the import-export feature, which is off by default. Add it to enabledFeatures in _features.conf. With the feature off, neither endpoint is registered.

An export file contains secrets in the clear: private key material held by the organization's key management services, stored credentials, password hashes, and API key hashes. Treat the file like a database backup — store it encrypted — and grant the export permission only to those who may see every secret of the organization. Every export is logged with the path and the account that ran it.

Prerequisites

Before you begin, ensure you have:

  • The import-export feature enabled.
  • A Bearer token for an account that holds the relevant permission on the organization or tenant you are exporting or importing.
  • On the target installation, a superadmin-created organization with the same id as the source. Organization ids are not renamed. For a tenant import, that organization must already exist on the target.

When performing operations within an organization, use the organization's Base URL or another valid host alias. For the sandbox organization test, the base URL is https://test.enterprise-sandbox.waltid.dev.

What an Export Contains

ContentOrganization exportTenant export
Tree entries the caller may read (services, keys, DIDs, credentials, sessions, roles, API keys, …)The whole organizationThe tenant and below
The organization's own data: profile, billing, configurationYes—
Host aliases pointing into itAll of the organization'sThose pointing into the tenant
Accounts holding roles in it, with only those rolesYesOnly roles inside the tenant
Their logins (identifiers and password hashes)YesYes
The accounts behind its API keysYesYes
EventsWith include=eventsWith include=events, those of the tenant
The eIDAS audit chainYes, unless left outNever — it is one chain per organization

Never exported: flags such as superadmin, roles in other organizations, maintenance and ownership state (they belong to the installation), licence and usage data, and short-lived OAuth codes.

Permissions

PermissionAllowsImplies
organization-management-export-treeGET …/exportReading every entry type
organization-management-import-treePOST …/importReading the organization entry

Import writes the file's entries, accounts, and logins without checking them one by one against the caller's roles; the import permission is the authority. Because a file can contain roles, API keys, and accounts, the import permission amounts to full control of what is imported into. Grant it accordingly.


Export This Organization or Tenant

Both endpoints act on the organization or tenant they are called for (/v1/{orgID}/… or /v1/{orgID}.{tenantID}/…). The response body is the compressed file.

CURL

Endpoint: GET /v1/{target}/organization-management-api/export | API Reference

Example Request
curl -X 'GET' \
  'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/organization-management-api/export?include=audit,events' \
  -H 'accept: application/octet-stream' \
  -H 'Authorization: Bearer {yourToken}' \
  --output acme.jsonl.zst

Path Parameters

  • orgID: String (required) - Your organization ID. When performing operations within an organization, it is essential to use the organization's Base URL or another valid host alias. For example, if your organization is named test, your default Base URL is test.enterprise-sandbox.waltid.dev.
  • target: String (required) - The organization or tenant being exported, e.g. acme or acme.branch-1.

Query Parameters

  • compression: String (optional) - zstd (default) or gzip. Unknown values are refused with 400.
  • include: String (optional) - Comma-separated optional contents: events (off by default) and audit (on by default for an organization; pass include= without it to leave it out). The audit chain is never part of a tenant export.

Header Parameters

  • Authorization: String (required) - Bearer token for Enterprise Stack authentication. Format: Bearer {token}.

Example Response

The response body is the compressed JSON Lines file (.jsonl.zst or .jsonl.gz). Content-Disposition names the download.

Response Codes

  • 200 — The export file is streamed.
  • 400 — Unsupported compression or unknown include value.
  • 401 — Invalid or missing authentication token.
  • 403 — The caller does not hold organization-management-export-tree.

Import an Export File

Send the file from the export endpoint as the raw request body. Compression (zstd or gzip) is recognized from the file's magic bytes — you do not pass a compression parameter. Call it on what was exported: the same organization, or the same tenant path.

The whole file is checked before anything is written. Then nothing that exists is overwritten, so repeating an import is safe.

CURL

Endpoint: POST /v1/{target}/organization-management-api/import | API Reference

Example Request
curl -X 'POST' \
  'https://{orgID}.enterprise-sandbox.waltid.dev/v1/{target}/organization-management-api/import' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/octet-stream' \
  -H 'Authorization: Bearer {yourToken}' \
  --data-binary @acme.jsonl.zst

Path Parameters

  • orgID: String (required) - Your organization ID. When performing operations within an organization, it is essential to use the organization's Base URL or another valid host alias. For example, if your organization is named test, your default Base URL is test.enterprise-sandbox.waltid.dev.
  • target: String (required) - The organization or tenant being imported into. Must equal the export's root, e.g. acme or acme.branch-1.

Header Parameters

  • Authorization: String (required) - Bearer token for Enterprise Stack authentication. Format: Bearer {token}.

Body Parameters

  • The raw compressed export file. Content-Type is application/octet-stream.

Example Response
{
  "organization": "acme",
  "root": "acme",
  "inFile": {
    "organization": 1,
    "entries": 412,
    "hostAliases": 2,
    "accounts": 9,
    "logins": 7,
    "loginData": 7,
    "audit": 1830
  },
  "inserted": {
    "organization": 1,
    "entries": 409,
    "hostAliases": 1,
    "accounts": 8,
    "logins": 6,
    "loginData": 6,
    "audit": 1830
  },
  "skippedExisting": {
    "entries": 3,
    "hostAliases": 1,
    "accounts": 1,
    "logins": 1,
    "loginData": 1
  },
  "accountsMerged": 1,
  "warnings": []
}

Response Fields

  • organization: String - The organization id imported into.
  • root: String - The organization or tenant path imported into.
  • inFile: Object - Per kind of record, how many the file held.
  • inserted: Object - Per kind of record, how many were written.
  • skippedExisting: Object - Per kind of record, how many already existed and were left unchanged.
  • accountsMerged: Integer - Accounts that already existed and received only this export's roles.
  • warnings: Array of Strings - Installation-level references (for example resource-access config ids) that do not resolve on the target. Until they do, the services using them fail — revocation included.

Response Codes

  • 200 — Import finished. Counts report what was written and what was skipped.
  • 400 — Import refused; nothing was written. The response lists the first 20 problems, with line numbers.
  • 401 — Invalid or missing authentication token.
  • 403 — The caller does not hold organization-management-import-tree.
  • 503 — The organization is in maintenance mode and the caller does not hold the maintenance bypass permission.

🎉 You've imported an organization or tenant from an export file.

How an Import Behaves

The whole file is checked before anything is written. An import is refused with 400, and nothing is written, if:

  • The file is not a zstd or gzip compressed export, or its format version is not supported.
  • It is an export of something other than what it is imported into.
  • It has no closing summary, or the summary does not match its contents — it is incomplete or was altered.
  • An entry lies outside what is imported into, or names a parent other than the one its path implies.
  • A parent, a service dependency, or a role that an account holds is neither in the file nor already there — so a tenant export that depends on an organization-level KMS needs that KMS on the target.
  • An account holds roles in another organization, or a login belongs to an account not in the file.
  • A host alias is used by another organization on the target.
  • The eIDAS audit chain does not verify link by link.
  • An entry appears twice, or a line is not a valid record.

Nothing that exists is overwritten:

  • Entries, host aliases, events, and logins that exist are left unchanged.
  • An account that exists — with the same id, or for a person, the same e-mail address — keeps everything it has and only gains the file's roles; its logins are not imported, since it has its own.
  • The organization's profile, billing, and configuration replace what it was created with on the target; its maintenance and ownership state are kept.
  • The audit chain is restored exactly as it was — sequence numbers and hashes included, so it still verifies — and only into an organization that has no chain yet. Otherwise it is skipped.

So importing the same file twice is safe, and importing an older export restores what has been deleted since without reverting what has changed.

An import is not atomic across batches. If the database fails part way, what was written stays; running the same import again completes it, because that is then skipped. A partially restored eIDAS audit chain is skipped on re-import (the target already has entries); clear the partial chain before retrying if you need it restored.

The upload is held in a temporary file on the API pod for the duration of the import, because it is read twice. Size the pod's temporary storage for the largest organization you expect to import.

Moving an Organization to Another Installation

  1. On the source, put the organization (or tenant) into maintenance mode so the export is a consistent snapshot. Reading keeps working meanwhile. Import is a write and is refused while that window is active.
  2. Export it.
  3. On the target, a superadmin creates the organization with the same id. Organization ids must be free on the target, since nothing is renamed. For a tenant, the organization must exist there already.
  4. Import the file into it.
  5. Move the DNS names of the organization's host aliases to the target. The URLs in credentials already issued — DIDs (did:web), issuer and status list URLs — point to those names, so they keep working only if the names move with the organization.
  6. Make sure the target can reach what the export only references:
    • Status lists are published to object storage (S3, Azure, GCP), not stored in the database, and credentials already issued point to their URLs. Keep the bucket where it is and give the target access to it; nothing needs copying.
    • Keys held in an external KMS or HSM (AWS, Azure, PKCS#11).

    Both reach their storage through resource-access entries of the installation's configuration, which an export cannot carry. The import lists every such reference the target does not have in warnings (and logs them); add those entries to the target's resource-access configuration, or those services fail — revocation included.
  7. Check the result, then end maintenance mode on the source and, when all is well, delete it there.

To merge with an organization on the target installation afterwards, offer and accept ownership there (requires the organization-ownership feature).

File Format

JSON Lines, compressed with zstd (.jsonl.zst) or gzip (.jsonl.gz):

  • Line 1: a header — {"format":"waltid-organization-export","version":2,"organization":"acme","root":"acme","exportedAt":"..."}
  • Then one record per line: {"record":"entry","value":{...}}, with record one of organization, entry, host-alias, account, login, login-data, event, audit
  • Last: {"record":"summary","counts":{...},"externalDependencies":[...]}, listing how many records of each kind the file holds and the services outside the export that exported services depend on.

INFO is logged when an export or import starts and finishes, with the path, the account, the counts, and the time taken. WARN is logged when an import is refused, with the problems found.

Next Steps

  • Create Organization — Recreate the organization on the target before importing.
  • Host Aliases — Move DNS names so issued credential URLs keep resolving.
  • Features — Enable import-export in _features.conf.
Last updated on November 10, 2026