Portal Community

Export API

GET /api/tenants/{tenantId}/apps/{appId}/export

// Response: 200 OK
// Content-Type: application/json
// Body: the full export bundle JSON

// Example:
curl -X GET \
  -H "Authorization: Bearer {jwt_token}" \
  https://api.bizfirstai.com/api/tenants/acme/apps/crm/export \
  -o crm-app-export.json

// With version label and changelog (query params):
GET /api/tenants/{tenantId}/apps/{appId}/export?version=1.4.2&changelog=Release+notes+here

Import API

POST /api/tenants/{tenantId}/apps/import
Content-Type: application/json
Authorization: Bearer {jwt_token}

Request body:
{
  "bundle": { ...full bundle JSON... },
  "conflictStrategy": "replace",   // "replace" | "merge"
  "dryRun": false                   // true = validate only, do not apply
}

// Response: 200 OK
{
  "success": true,
  "appId": "crm",
  "version": "1.4.2",
  "created": 3,       // Elements created
  "updated": 12,      // Elements updated
  "deleted": 0,       // Elements deleted (replace only)
  "skipped": 0        // Validation skipped
}

// Response: 400 Bad Request (validation failure)
{
  "success": false,
  "errors": [
    "Page 'lead-detail': breadcrumbParent 'missing-page' not found in bundle",
    "Widget 'leads-grid': duplicate widgetId in pane 'main-pane'"
  ]
}

Dry Run — Validate Without Applying

// Use dryRun: true to validate the bundle and preview changes
// without writing anything to the database
POST /api/tenants/{tenantId}/apps/import

{
  "bundle": { ... },
  "conflictStrategy": "replace",
  "dryRun": true
}

// Response shows what would happen:
{
  "success": true,
  "dryRun": true,
  "appId": "crm",
  "preview": {
    "pagesCreated": 0,
    "pagesUpdated": 8,
    "widgetsCreated": 2,
    "widgetsUpdated": 43,
    "widgetsDeleted": 1
  }
}

Required Permissions

APIRequired role
GET exportapp-studio-admin or platform-admin on the tenant
POST importapp-studio-admin or platform-admin on the tenant

CI/CD Pipeline Integration

# GitHub Actions — promote app to staging on tag push
name: Promote CRM App to Staging

on:
  push:
    tags:
      - 'app-crm-v*'

jobs:
  promote:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Export from Dev
        run: |
          curl -s -X GET \
            -H "Authorization: Bearer $DEV_TOKEN" \
            $DEV_API/api/tenants/acme-dev/apps/crm/export \
            -o crm-bundle.json
        env:
          DEV_TOKEN: ${{ secrets.DEV_API_TOKEN }}
          DEV_API: ${{ vars.DEV_API_URL }}

      - name: Import to Staging
        run: |
          BUNDLE=$(cat crm-bundle.json)
          curl -s -X POST \
            -H "Authorization: Bearer $STAGING_TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"bundle\": $BUNDLE, \"conflictStrategy\": \"replace\"}" \
            $STAGING_API/api/tenants/acme-staging/apps/import
        env:
          STAGING_TOKEN: ${{ secrets.STAGING_API_TOKEN }}
          STAGING_API: ${{ vars.STAGING_API_URL }}