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_urlof 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_overrideorallow_path_overrideswitched off refuses a request whose branch or path differs from the stored one - an omitted
pathmeans 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/validatetakes the same body as an import, withouttarget, plus a requiredsource_typeofgitorfile. It answers withvalid, theerrors[]andwarnings[]arrays (each entry carrieslevel, a machinecode,message,elementandelement_type), asummarywith the counts ofmeasures,dimensions,facts,fact_tablesanddata_marts, andschema_versionimport/previewcompares the source against the version named in the path and answers withpreview_id, asummaryover the same five sections (added,modified,deleted,total),conflicts[]andconflict_resolution_required. On previewsource_typemay 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 whilevalidisfalse - import into the staging environment with
import/git, usually withtarget.methodset tocreateand 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_ACCESSentry in the audit log, with before and after snapshots for writes - The multipart endpoints —
import/fileand the file flavor ofimport/validateandimport/preview— work only in on-premises installations - A connection created through
save_connectionis private: it is visible to its creator and to administrators, and sharing it is an administrator's decision