Skip to main content

Organization Import / Export

This document describes both export/import formats for organization data: the JSON envelope (full fidelity, for programmatic use) and the CSV ZIP (human-readable spreadsheets, for manual editing and bulk data operations).


Overview

Both formats capture the full state of an organization:

IncludedDescription
Organization settingsLicense dates, flags, schema version, default activity attributes
LocalesOne row/entry per ROPA locale (locale code, display name, default flag)
PartnersAll partner organizations with addresses and contacts
ContractsAll contracts with linked partner and activity IDs
ROPA dataAll processing activities, one document per locale
TemplatesHandlebars templates (JSON only — not suitable for CSV)

Import always replaces the organization if shortName already exists (delete + recreate, returns isError: false). If the org does not exist, it is created fresh.


Routes

All routes require the x-admin-secret HTTP header matching the ADMIN_SECRET environment variable.

JSON Export

GET /api/admin/org/export?shortName=<shortName>

Returns <shortName>-export.json.

curl -H "x-admin-secret: $ADMIN_SECRET" \
"https://your-host/api/admin/org/export?shortName=acme" \
-o acme-export.json

JSON Import

POST /api/admin/org/import
Content-Type: application/json

Body: the JSON file produced by the export route.

curl -X POST \
-H "x-admin-secret: $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d @acme-export.json \
"https://your-host/api/admin/org/import"

CSV ZIP Export

GET /api/admin/org/export?shortName=<shortName>&format=csv

Returns <shortName>-export.zip.

curl -H "x-admin-secret: $ADMIN_SECRET" \
"https://your-host/api/admin/org/export?shortName=acme&format=csv" \
-o acme-export.zip

CSV ZIP Import

POST /api/admin/org/import
Content-Type: multipart/form-data

Body: multipart field file containing the ZIP file.

curl -X POST \
-H "x-admin-secret: $ADMIN_SECRET" \
-F "file=@acme-export.zip" \
"https://your-host/api/admin/org/import"

Response codes (both formats)

StatusReason
200Success — JSON body with { ok, shortName, orgId, message }
401Missing or invalid x-admin-secret
400Invalid body / bad ZIP archive
422Validation failed

JSON Envelope Format

Structure

{
"exportVersion": 1,
"exportedAt": "2026-04-05T10:00:00.000Z",
"organization": {
"shortName": "acme",
"clerkOrganizationId": "org_abc123",
"licenseStart": 1700000000000,
"licenseEnd": 1731556952000,
"licenseCost": 0,
"isBlocked": false,
"isPublic": true,
"isDemo": false,
"highestOuId": 4,
"highestActivityId": 6,
"highestPartnerId": 2,
"highestContractId": 1,
"schemaVersion": 13,
"defaultActivityAttributes": {},
"ropas": [
{ "locale": "en", "longName": "ACME Corp ROPA", "isDefault": true }
],
"partners": [ ... ],
"contracts": [ ... ],
"templates": [
{ "activityId": 0, "type": "ropaFrontPage" }
]
},
"ropas": [
{
"orgShortName": "acme",
"locale": "en",
"ous": [
{
"ouId": 1,
"ouName": "Marketing",
"ouColor": "#ff6600",
"activities": [ ... ]
}
]
}
],
"templates": [
{
"orgShortName": "acme",
"locale": "en",
"activityId": 0,
"type": "ropaFrontPage",
"description": "Front page template",
"handlebars": "<h1>{{organizationName}}</h1>..."
}
]
}

Serialization rules

  • All MongoDB _id / __v fields are stripped.
  • ropaId references inside organization.ropas[] are stripped — resolved at import time by locale key.
  • templateId references inside organization.templates[] are stripped — rebuilt at import.
  • orgId is stripped from each template — linked at import via orgShortName.
  • organizationLogo Buffers are base64-encoded strings in the export; decoded back to Buffers on import.
  • Embedded contract subdocument _id fields are stripped.

CSV ZIP Format

File inventory

A ZIP produced by the CSV export contains the following files, all prefixed with {shortName}-:

FileDescription
{shortName}-organization.csvOne header row + one data row for org scalar fields
{shortName}-locales.csvOne row per locale
{shortName}-partners.csvOne row per partner organization
{shortName}-contracts.csvOne row per contract
{shortName}-ropa-{locale}.csvOne file per locale; one row per activity
{shortName}-templates.jsonHandlebars templates (only present if templates exist)

All CSV files use UTF-8 encoding, LF line endings, and RFC-4180 quoting (fields containing commas, double-quotes, or newlines are wrapped in double-quotes; internal double-quotes are escaped as "").

Multi-valued fields within a single cell are pipe-separated (|).

organization.csv columns

ColumnTypeNotes
shortNamestringMust match ZIP filename prefix
clerkOrganizationIdstringClerk org ID
licenseStartnumberUnix timestamp (ms)
licenseEndnumberUnix timestamp (ms)
licenseCostnumber
isBlockedbooleantrue / false
isPublicboolean
isDemoboolean
highestOuIdnumberMonotonic counter used to prevent ID reuse
highestActivityIdnumber
highestPartnerIdnumber
highestContractIdnumber
schemaVersionnumber
defaultActivityAttributesJSON stringSerialized object

locales.csv columns

ColumnTypeNotes
localestringBCP-47 language tag (e.g. en, fr)
longNamestringDisplay name for the ROPA document
isDefaultbooleanExactly one row must be true

partners.csv columns

ColumnTypeNotes
organizationIdnumber0 = the org itself (self-row, always required)
organizationNamestringShort name
organizationNameLongstringFull legal name
organizationColorstringHex color or empty
organizationWebsitestringURL or empty
addressLine1stringFlattened from organizationPostalAddress
addressLine2string
citystring
stateProvincestring
postalCodestring
countrystring
organizationLogostringBase64-encoded image or empty
organizationNotesstring
organizationContactsJSON stringArray of contact objects
contractOrderpipe-sep numbersOrdered contract IDs for this partner

contracts.csv columns

ColumnTypeNotes
contractIdnumberMust be ≤ highestContractId
contractNamestring
contractUrlstring
contractExpirationDatestringISO date string or empty
contractDescriptionstring
activityIdspipe-sep numbersActivity IDs covered by this contract
partnerIdspipe-sep numbersPartner IDs party to this contract

ropa-{locale}.csv columns

One file per locale. Each row represents one activity nested inside an OU.

ColumnTypeNotes
localestringMust match the filename locale
ouIdnumberMust be ≤ highestOuId
ouNamestring
ouColorstring
activityIdnumberMust be ≤ highestActivityId
activityNamestring
purposeShortstring
purposeLongstring
legalbasispipe-sepArray field
legalbasisLongstring
legalbasisSpecialpipe-sepArray field
dataCategoriespipe-sepArray field
datasubjectCategoriesstring
activityCategoriespipe-sepArray field
dataOriginstring
timeLimitstring
profilingboolean
communicationsstring
communicationsLongstring
controllerspipe-sepArray field
processorspipe-sepArray field
transfersboolean
transfersLongstring
securityLevelstring
securityMeasuresLongstring
activeboolean
timestampnumberUnix timestamp (ms)

templates.json

Same structure as the templates[] array in the JSON envelope. Only present in the ZIP when the organization has at least one template. Not converted to CSV because Handlebars source contains HTML markup that is not spreadsheet-friendly.


ZIP Integrity Checks

Before import, the client-side zipCheck function validates:

  • *-organization.csv is present and has exactly one data row
  • shortName in the CSV matches the filename prefix
  • *-locales.csv, *-partners.csv, *-contracts.csv are present
  • One *-ropa-{locale}.csv exists for every locale listed in locales.csv
  • No unexpected files appear in the ZIP
  • All locales in ropa filenames are listed in locales.csv
  • locales.csv has exactly one isDefault=true row
  • partners.csv includes a self-org row (organizationId=0)
  • All organizationId values in partners.csv are ≤ highestPartnerId
  • All contractId values in contracts.csv are ≤ highestContractId
  • All partnerIds in contracts.csv are ≤ highestPartnerId
  • All ouId / activityId values in ropa CSVs are within their respective highest* bounds
  • All activityIds referenced in contracts.csv exist in at least one ropa CSV

Import Process

JSON import

1. Validate envelope against Zod schema (exportVersion, required fields)
2. If org already exists: delete templates → ROPAs → org
3. Create ROPA documents — one per locale → collect { locale → ObjectId } map
4. Create Organization — links ropas[] via the locale map
5. Create Templates — each gets a new _id linked to the new org _id
6. Patch org.templates[] with the new templateId references

CSV ZIP import

1. Parse organization.csv → orgData
2. Parse locales.csv → locales[]
3. Parse partners.csv → partners[]
4. Parse contracts.csv → contracts[]
5. Parse ropa-{locale}.csv for each locale → ropas[]
6. Parse templates.json if present → templates[]
7. Reconstruct JSON envelope and delegate to importOrganization()

Step 7 reuses the identical JSON import pipeline, so validation, delete-before-recreate, and creation order are the same for both formats.

If any step fails, already-created documents are not automatically rolled back. To recover from a partial import, delete the partially-created organization and retry.


Migrating Between Environments

# Export from staging (JSON)
curl -H "x-admin-secret: $STAGING_SECRET" \
"https://staging.example.com/api/admin/org/export?shortName=acme" \
-o acme-export.json

# Import into production
curl -X POST \
-H "x-admin-secret: $PROD_SECRET" \
-H "Content-Type: application/json" \
-d @acme-export.json \
"https://prod.example.com/api/admin/org/import"

Source Files

FilePurpose
src/services/exportService.jsexportOrganization (JSON) and exportOrganizationAsCsvZip (CSV ZIP)
src/services/importService.jsimportOrganization (JSON) and importOrganizationFromCsvZip (CSV ZIP)
src/services/csvService.jsCSV serializers/deserializers for all entity types
src/lib/utils/fileChecks.jsisValidJson, isValidZip, zipCheck
src/app/api/admin/org/export/route.jsExport route handler (both formats)
src/app/api/admin/org/import/route.jsImport route handler (both formats)