Skip to content

Version transfer via API

The version transfer endpoints of Public API v2 export a project version into a Git repository or into an archive file and import it back, so that a script or a CI/CD pipeline can move a DataForge configuration between environments without opening the interface; the whole surface is served under the base path /df-api/v2 and is authenticated with an API key.

General information

Property Value
Base path /df-api/v2
Authentication X-Api-Key header (see the API keys page)
Path prefix {v} below stands for /projects/{project_id}/versions/{version_id}
Required access Analyst or higher for export and dry runs, developer or higher for import, Administrator for Git connections
Request limit 100 requests per 60 seconds per key
License Available only while the calling company holds a valid license
Audit Every call is recorded in the audit log, with a snapshot before and after the change for writes

The same export and import engine is available in the interface — see the Project version export and import page for what a version contains, how the file tree is laid out, and how merged Git branches are handled.

Endpoints

Method Path Required access Description
POST {v}/export/git Analyst or higher Export the version as a commit into a Git repository
POST {v}/export/file Analyst or higher Export the version into a .dfexport.zip archive and return a signed download link
POST {v}/import/validate Analyst or higher Validate an import source without writing anything
POST {v}/import/preview Analyst or higher Show what an import would change, without writing anything
POST {v}/import/git Developer or higher Import a configuration from a branch of a Git repository
POST {v}/import/file Developer or higher Import a configuration from an uploaded archive (multipart request)

The required access is the effective project role of the API key owner. Export and the dry runs require analyst or higher — stricter than in the interface, where a viewer may export as well. If the key owner has no access to the project at all, the answer is 404 with the code DF_API.PROJECT_NOT_FOUND, and the existence of the project is not disclosed.

Every fact table and data mart that import/git or import/file creates, in both the create and replace modes, belongs to the API key owner, as if they had created it by hand; the owner recorded in the export is not used.

Git credentials

Credentials for a repository are supplied in one of two ways: inline in the authentication object, or by referencing a saved Git connection through connection_id.

Field Method Description
authentication.method — pat, ssh or password
authentication.token pat Personal access token
authentication.private_key, authentication.passphrase ssh SSH private key and, optionally, its passphrase
authentication.username, authentication.password password Login and password

Rules that apply when a saved connection is used:

  • the target repository is always the repository_url of the request; a saved connection contributes only its credentials
  • the credentials of a connection are accepted only for the host of that connection
  • a connection with allow_branch_override or allow_path_override switched off refuses a request whose branch or path differs from the stored one
  • an omitted path means the path stored on the connection

Credentials are never returned in a response — tokens, passwords and private keys are not part of any response schema — and credentials embedded into repository_url are removed from the response, the audit record and any saved connection.

Exporting a project version

The body of export/git carries repository_url, branch and commit_message as required fields, plus path, either connection_id or authentication, and options. The body of export/file carries options only.

Option Default Description
include_rmd, include_fact_tables, include_data_marts, include_connections true Which sections the export carries
include_history false Include the RMD cell change history
encrypt_sensitive true The value false is refused with 400: connection passwords are never exported in cleartext
add_gitattributes true export/git only: place a .gitattributes file into the tree
save_connection, connection_name false export/git only: store the credentials passed in authentication as a new private Git connection under the given name
encryption_password, use_system_key — export/file only: wrap the archive in an encrypted envelope; the password takes precedence

export/git answers with success, commit_hash, repository_url, branch, path, files_created, timestamp and connection_id. export/file answers with download_url, file_name, file_size and expires_at; the link is signed and expires.

The export is deterministic — re-exporting an unchanged version produces the same tree; only the manifest differs by its export timestamp, and a difference in the manifest alone counts as no change. A repeated export/git therefore creates no commit and answers commit_hash: null, and the branch is never overwritten: changes land as ordinary commits on top of its history.

Validating and previewing an import

Both endpoints are dry runs and write nothing, which makes them the natural gate in a pipeline before a deployment.

  • import/validate takes the same body as an import, without target, plus a required source_type of git or file. It answers with valid, the errors[] and warnings[] arrays (each entry carries level, a machine code, message, element and element_type), a summary with the counts of measures, dimensions, facts, fact_tables and data_marts, and schema_version
  • import/preview compares the source against the version named in the path and answers with preview_id, a summary over the same five sections (added, modified, deleted, total), conflicts[] and conflict_resolution_required. On preview source_type may be omitted — the type is read from the request itself

Each entry of conflicts[] describes one field: element_type, element_id, element_name, field, current_value and imported_value.

A git source is sent as JSON; a file source is sent as a multipart request with a file part.

Importing a version

The body of import/git carries repository_url and branch as required fields, plus path, commit_hash (to pin one commit instead of the branch tip), either connection_id or authentication, target, conflict_strategy and options. The body of import/file is multipart: a file part with a .dfexport.zip archive, the flat text fields target.*, conflict_strategy and encryption_password, and options as a JSON string.

Target version

Field Description
target.method create (default) — create a new version, subject to the license limit on the number of versions; replace — fully replace the version named in the path
target.version_name Required. The name of the new version, or the new name of the replaced one

The version name follows the same rules as in the interface: up to 64 characters, letters, digits, underscore, space, dot and dash. A missing name answers 400 with the code DF_API.MISSING_REQUIRED_FIELD, and a name already taken by another live version of the project answers 409 with the code duplicate_name.

In both modes the version named in the path is the baseline that the conflict strategy compares against.

Conflict handling

conflict_strategy acts element by element:

Strategy Behavior
smart_merge (default) Additions and modifications from the source land; elements that only the current version has survive
overwrite The source lands verbatim, deletions included
skip Conflicting elements keep their current values; additions land, deletions do not
manual If conflicts are present, nothing is written: the answer is success: false together with the conflict list

Import options

Option Default Description
import_rmd, import_fact_tables, import_data_marts, import_connections true Which sections are applied; a switched-off section stays as the current version has it
import_history false Apply the RMD change history if the source carries it
detect_merges true Carry the merge provenance recorded in the source manifest
decrypt_sensitive true With false, connections are imported without passwords and end up in the state that requires a refresh
encryption_password — The password of a sealed archive

The import answers with success, import_id, target_project_id, target_version_id, target_version_name (identifiers as strings), a summary of what was actually applied over the five sections, conflicts[] and conflict_resolution_required.

Repeated requests

An import is idempotent by the Idempotency-Key header, whose value must be a UUID version 4: a retried request lands on the same version and returns the same import_id instead of creating a second version. Use a stable key derived from the pipeline run, not a random one per attempt. The endpoints export/file, import/validate, import/preview and the connection test deliberately do not replay a stored response — a cached answer would be stale, such as an expired link or an outdated check.

Managing Git connections

The company's registry of saved Git connections is available under /df-api/v2/git-connections. Every endpoint requires the Administrator or Super administrator role; a resource the key cannot see answers 404, an insufficient role answers 403.

Method Path Description
GET /git-connections List the company's connections; page and pageSize up to 100, a larger value is refused with page_size_exceeded
POST /git-connections Create a connection (201); the connection is tested before it is stored, and an unreachable repository is refused
GET /git-connections/{connection_id} Get one connection
PUT /git-connections/{connection_id} Update a connection; an absent authentication keeps the stored credentials, while repointing to another host requires entering them again
DELETE /git-connections/{connection_id} Delete a connection (204); versions imported through it survive and only lose the reference
POST /git-connections/{connection_id}/test Test a connection and refresh its stored status

The create body carries name, platform, repository_url, branch and authentication as required fields, plus the optional path and settings. The vocabulary accepted on write is narrower than the one that may appear on read:

Field On write Additionally on read
platform github, gitlab, bitbucket, azure-devops, generic github-enterprise
authentication.method pat, ssh, password — (responses never contain authentication)
status — active when the check passed, failed otherwise
Setting Default Description
settings.allow_branch_override true Allow an export or import through this connection to name a different branch
settings.allow_path_override true The same for the path within the repository
settings.set_as_default false Make it the company's default connection, displacing the previous one
settings.share_with_all false Make it usable by every user of the company; a connection that is not shared is visible only to its creator and to administrators

A connection object in a response carries id, name, platform, repository_url, branch, path, status, last_used (stamped by exports and imports run through the connection), created_at, created_by, shared and default.

The test answers with git-connection_id, status (success or failed), tests — five checks: repository_reachable, authentication_valid, branch_exists and write_permission form a ladder where each implies the previous ones, and path_accessible is checked separately (the path may be missing in a repository that is otherwise writable) — details (last_commit, last_commit_date, available_branches) and tested_at. An empty repository is a valid export target: the checks pass and last_commit is null.

Using the API in a CI/CD pipeline

The endpoints let a DataForge configuration be managed as code: changes are exported to Git, reviewed through merge requests, validated by the pipeline, and rolled out across environments. A typical pipeline runs four stages:

  • export the source version with export/git, so that every change is committed to the configuration repository
  • validate the branch against the target environment with import/validate, and stop the pipeline while valid is false
  • import into the staging environment with import/git, usually with target.method set to create and a version name derived from the pipeline run
  • import into production with the same call, behind a manual approval

Values that a pipeline normally keeps as variables:

Variable Description
Base URL The public API base URL, for example https://dataforge.example.com/df-api/v2
API key One key per environment, marked as masked in the CI settings
Source project and version identifiers The version being exported
Target project and version identifiers The baseline version of each target environment
Repository URL, branch and path The configuration repository coordinates; do not name the path variable PATH, as it would clobber the system path of a shell runner
Git credentials The token, key or password used by the pipeline, marked as masked in the CI settings

Alternatively the pipeline sends connection_id instead of authentication, and the Git credentials never leave DataForge.

GitLab CI example

# .gitlab-ci.yml
stages:
  - export
  - validate
  - deploy-staging
  - deploy-production

export-config:
  stage: export
  script:
    - |
      curl --fail-with-body -X POST "${DATAFORGE_URL}/projects/${PROJECT_ID}/versions/${VERSION_ID}/export/git" \
        -H "X-Api-Key: ${DATAFORGE_API_KEY}" \
        -H "Content-Type: application/json" \
        -d '{
          "repository_url": "'"${REPO_URL}"'",
          "branch": "'"${BRANCH}"'",
          "path": "'"${REPO_PATH}"'",
          "commit_message": "Export from DataForge, pipeline '"${CI_PIPELINE_ID}"'",
          "authentication": { "method": "pat", "token": "'"${GIT_TOKEN}"'" }
        }'
  only:
    - main

validate-config:
  stage: validate
  script:
    - |
      RESPONSE=$(curl --fail-with-body -sS -X POST "${DATAFORGE_URL}/projects/${STAGING_PROJECT_ID}/versions/${STAGING_VERSION_ID}/import/validate" \
        -H "X-Api-Key: ${STAGING_DATAFORGE_API_KEY}" \
        -H "Content-Type: application/json" \
        -d '{
          "source_type": "git",
          "repository_url": "'"${REPO_URL}"'",
          "branch": "'"${BRANCH}"'",
          "path": "'"${REPO_PATH}"'",
          "authentication": { "method": "pat", "token": "'"${GIT_TOKEN}"'" }
        }')
    - echo "${RESPONSE}"
    - test "$(echo "${RESPONSE}" | jq -r '.valid')" = "true"
  only:
    - main

deploy-staging:
  stage: deploy-staging
  script:
    - |
      H=$(echo -n "dataforge-${CI_PIPELINE_ID}-staging" | md5sum | cut -c1-32)
      IDEMPOTENCY_KEY="${H:0:8}-${H:8:4}-4${H:13:3}-8${H:17:3}-${H:20:12}"
    - |
      RESPONSE=$(curl --fail-with-body -sS -X POST "${DATAFORGE_URL}/projects/${STAGING_PROJECT_ID}/versions/${STAGING_VERSION_ID}/import/git" \
        -H "X-Api-Key: ${STAGING_DATAFORGE_API_KEY}" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
        -d '{
          "repository_url": "'"${REPO_URL}"'",
          "branch": "'"${BRANCH}"'",
          "path": "'"${REPO_PATH}"'",
          "target": { "method": "create", "version_name": "staging_'"${CI_PIPELINE_ID}"'" },
          "conflict_strategy": "smart_merge",
          "authentication": { "method": "pat", "token": "'"${GIT_TOKEN}"'" }
        }')
    - echo "${RESPONSE}"
    - test "$(echo "${RESPONSE}" | jq -r '.success')" = "true"
  only:
    - main
  environment:
    name: staging

deploy-production:
  stage: deploy-production
  script:
    - |
      H=$(echo -n "dataforge-${CI_PIPELINE_ID}-production" | md5sum | cut -c1-32)
      IDEMPOTENCY_KEY="${H:0:8}-${H:8:4}-4${H:13:3}-8${H:17:3}-${H:20:12}"
    - |
      RESPONSE=$(curl --fail-with-body -sS -X POST "${DATAFORGE_URL}/projects/${PRODUCTION_PROJECT_ID}/versions/${PRODUCTION_VERSION_ID}/import/git" \
        -H "X-Api-Key: ${PRODUCTION_DATAFORGE_API_KEY}" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
        -d '{
          "repository_url": "'"${REPO_URL}"'",
          "branch": "'"${BRANCH}"'",
          "path": "'"${REPO_PATH}"'",
          "target": { "method": "create", "version_name": "production_'"${CI_PIPELINE_ID}"'" },
          "conflict_strategy": "smart_merge",
          "authentication": { "method": "pat", "token": "'"${GIT_TOKEN}"'" }
        }')
    - echo "${RESPONSE}"
    - test "$(echo "${RESPONSE}" | jq -r '.success')" = "true"
  only:
    - main
  when: manual
  environment:
    name: production

Idempotency-Key must be a UUID v4; the example derives it deterministically from the pipeline id, so a retried failed job sends the same key and lands on the same created version instead of creating a second one.

GitHub Actions example

# .github/workflows/dataforge-deploy.yml
name: DataForge deploy

on:
  push:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Validate configuration
        run: |
          RESPONSE=$(curl --fail-with-body -sS -X POST "${{ vars.DATAFORGE_URL }}/projects/${{ vars.STAGING_PROJECT_ID }}/versions/${{ vars.STAGING_VERSION_ID }}/import/validate" \
            -H "X-Api-Key: ${{ secrets.STAGING_DATAFORGE_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{
              "source_type": "git",
              "repository_url": "${{ vars.REPO_URL }}",
              "branch": "main",
              "authentication": { "method": "pat", "token": "${{ secrets.GIT_TOKEN }}" }
            }')
          echo "${RESPONSE}"
          test "$(echo "${RESPONSE}" | jq -r '.valid')" = "true"

  deploy-staging:
    needs: validate
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Import into staging
        run: |
          H=$(echo -n "dataforge-${{ github.run_id }}-staging" | md5sum | cut -c1-32)
          IDEMPOTENCY_KEY="${H:0:8}-${H:8:4}-4${H:13:3}-8${H:17:3}-${H:20:12}"
          RESPONSE=$(curl --fail-with-body -sS -X POST "${{ vars.DATAFORGE_URL }}/projects/${{ vars.STAGING_PROJECT_ID }}/versions/${{ vars.STAGING_VERSION_ID }}/import/git" \
            -H "X-Api-Key: ${{ secrets.STAGING_DATAFORGE_API_KEY }}" \
            -H "Content-Type: application/json" \
            -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
            -d '{
              "repository_url": "${{ vars.REPO_URL }}",
              "branch": "main",
              "target": { "method": "create", "version_name": "staging_${{ github.run_number }}" },
              "conflict_strategy": "smart_merge",
              "authentication": { "method": "pat", "token": "${{ secrets.GIT_TOKEN }}" }
            }')
          echo "${RESPONSE}"
          test "$(echo "${RESPONSE}" | jq -r '.success')" = "true"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production   # manual approval is configured on the GitHub environment
    steps:
      - name: Import into production
        run: |
          H=$(echo -n "dataforge-${{ github.run_id }}-production" | md5sum | cut -c1-32)
          IDEMPOTENCY_KEY="${H:0:8}-${H:8:4}-4${H:13:3}-8${H:17:3}-${H:20:12}"
          RESPONSE=$(curl --fail-with-body -sS -X POST "${{ vars.DATAFORGE_URL }}/projects/${{ vars.PRODUCTION_PROJECT_ID }}/versions/${{ vars.PRODUCTION_VERSION_ID }}/import/git" \
            -H "X-Api-Key: ${{ secrets.PRODUCTION_DATAFORGE_API_KEY }}" \
            -H "Content-Type: application/json" \
            -H "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
            -d '{
              "repository_url": "${{ vars.REPO_URL }}",
              "branch": "main",
              "target": { "method": "create", "version_name": "production_${{ github.run_number }}" },
              "conflict_strategy": "smart_merge",
              "authentication": { "method": "pat", "token": "${{ secrets.GIT_TOKEN }}" }
            }')
          echo "${RESPONSE}"
          test "$(echo "${RESPONSE}" | jq -r '.success')" = "true"

Handling errors in a pipeline

Situation How to detect it What to do
The source is invalid import/validate answers valid: false and lists the problems in errors[] with machine codes such as REQUIRED_FILE_MISSING, DUPLICATE_ID Stop the pipeline before deploying; warnings in warnings[], for example REFERENCE_MISSING or CIRCULAR_DEPENDENCY, do not block a deployment
The import found conflicts The answer carries conflict_resolution_required: true and conflicts[] with current_value and imported_value With smart_merge, overwrite or skip the conflicts were already resolved by the chosen strategy and are reported for the record; with manual the import writes nothing and the pipeline can surface the conflicts for a human decision
A check before merging import/preview returns the same difference without writing Useful in a merge request job: show the summary and conflicts before the branches are merged
The request failed 400 for an invalid body, 404 when the project or version does not exist or is not visible to the key, 422 when Git is unreachable or rejected the credentials or the source is invalid, 429 when the quota is exceeded The error envelope carries a machine code and details[]; on 429, wait until the moment given in the X-RateLimit-Reset header (Unix time in seconds)
A failed job is retried An import with the same Idempotency-Key returns the original answer and creates no second version Use a stable key, such as one derived from the pipeline run identifier

Error codes specific to version transfer: DF_API.GIT_CONNECTION_FAILED (422) when the repository is unreachable or the credentials were rejected, DF_API.GIT_PUSH_FAILED (422) when the push was rejected because the branch changed during the export or is protected, and DF_API.UNPROCESSABLE_ENTITY (422) when the import source is invalid or exceeds the size limits.

Notes and limitations

  • Operations performed through the public API do not appear in the version export and import journal shown in the interface; each of them is recorded as an API_ACCESS entry in the audit log, with before and after snapshots for writes
  • The multipart endpoints — import/file and the file flavor of import/validate and import/preview — work only in on-premises installations
  • A connection created through save_connection is private: it is visible to its creator and to administrators, and sharing it is an administrator's decision