Docs/Developer/Atlas API - Branches and Commits
DeveloperAPI

Atlas API - Branches and Commits

The routes that create, compare, merge and delete branches and read commit history, with the request and response shape of each, and the review workflow they support for an automated import.

Updated 4 Nov 2026Timebase Docs

Overview#

Atlas keeps every domain and library in a Git repository, and every API path that reads or writes model content names the branch it acts on. The routes on this page manage the branches themselves: list, create, rename and delete them, read a branch's history, compare two branches, and merge one into another. Two more routes read a single commit: the files it changed and a file's content before and after.

Together they support the workflow an automated import wants: write each run to its own branch, compare it against the trunk, have a person approve it, merge, recompile, and keep the history. The last section walks that workflow end to end.

What the Atlas interface does on its Git page is described in Branch Management. Everything that page does is available here, and nothing here is available only in the interface.


Base path and authentication#

Two route groups:

/api/v1/branches
/api/v1/commits

Every route requires a Pulse-issued bearer token whose audience includes Atlas. The first call for a new client is described in Calling the Atlas API from Your Own Application.

Branch names with a slash are a single route segment, so encode the slash as %2F: feature/sync-2026-11-04 is addressed as /api/v1/branches/feature%2Fsync-2026-11-04. Atlas decodes the segment itself, so the encoded form is the only form that works.

Responses are camelCase and omit members whose value is null rather than emitting null. Three fields on this surface are affected and are called out below: upstream on a branch, nextCursor on the last page of history, and conflictFiles on a merge that succeeded.


Route table#

Method Route Purpose
GET /api/v1/branches List branches
POST /api/v1/branches Create a branch
PATCH /api/v1/branches/{branch} Rename a branch
DELETE /api/v1/branches/{branch}?force= Delete a branch
GET /api/v1/branches/{branch}/commits Commit history, cursor-paged
GET /api/v1/branches/{branch}/compare?base= Ahead and behind counts and changed files against another branch
POST /api/v1/branches/{branch}/merge Merge this branch into a target
GET /api/v1/commits/{sha}/files The files one commit changed
GET /api/v1/commits/{sha}/diff?path=&base= One file's content before and after a commit

The default branch#

One branch is the default: the trunk that merges target by default, that ahead and behind counts are measured against, and that can be neither renamed nor deleted. Atlas resolves it on the server: the DefaultBranch setting in the Git section of settings.config when that branch exists, otherwise main, otherwise master, otherwise the branch that has existed longest. The branch list marks it with isDefault. Read that flag rather than assuming the name.


List branches#

GET /api/v1/branches

200 OK

[
  { "name": "main", "isCurrent": true, "isDefault": true },
  { "name": "feature/sync-2026-11-04", "isCurrent": false, "isDefault": false }
]
Field Type Description
name string The branch name as Git holds it
isCurrent boolean Whether the repository is parked on this branch. This is a side effect of how Atlas writes, not a setting, and nothing you build should depend on it
isDefault boolean Whether this is the default branch. Exactly one branch is true whenever any branch exists
upstream string The tracking branch, when one is configured. Omitted otherwise

Private draft branches, the ones that hold a person's unpublished editing session, never appear in this list and cannot be addressed by the routes below.


Create a branch#

POST /api/v1/branches
Content-Type: application/json
{ "name": "feature/sync-2026-11-04", "from": "main" }
Field Type Required Description
name string Yes The new branch's name. Git's own rules apply: no spaces, no .., //, @{, or any of \ ~ ^ : ? * [, no leading - or /, no trailing / or ., and no component that starts with . or ends with .lock. A name that breaks a rule is refused with 400 and a detail naming the rule
from string No The branch or commit the new branch starts from. Always pass it. When it is omitted, Git starts the branch at whichever commit the repository happens to be parked on, which is a side effect of Atlas's own writes rather than a choice you made

201 Created, with Location: /api/v1/branches/feature%2Fsync-2026-11-04:

{ "name": "feature/sync-2026-11-04" }
Status When Detail
400 The name breaks a naming rule For example A branch name cannot contain a space.
409 The name is in use A branch named 'feature/sync-2026-11-04' already exists.

A branch is cheap. It shares every commit with the branch it came from and adds nothing until the first write lands on it.


Rename a branch#

PATCH /api/v1/branches/{branch}
Content-Type: application/json
{ "newName": "feature/sync-2026-11-04-approved" }

200 OK

{ "name": "feature/sync-2026-11-04-approved" }
Status When Detail
400 The new name breaks a naming rule The rule, as on create
400 The branch is the default branch 'main' is the repository's default branch and cannot be renamed.
403 Either name is a private draft branch '…' is an editing-session draft branch and cannot be renamed or deleted directly. Discard the editing session instead.
404 The branch does not exist Branch 'feature/old' was not found.
409 The new name is in use A branch named '…' already exists.

Open browsers on the old name follow the rename. API callers do not: a pipeline that stored the old name must be told.


Delete a branch#

DELETE /api/v1/branches/{branch}?force=false

force is required on every call. There is no default; a request without it is refused as a bad request.

204 No Content on success. The branch and any commits that existed only on it are gone, and every compiled world cached on that branch is unloaded, so its instances stop counting toward the licence without a restart. Nothing on any other branch changes.

Status When Detail
403 The branch is the default branch, whatever force says 'main' is the default branch and cannot be deleted.
403 The branch is a private draft branch '…' is an editing-session draft branch and cannot be renamed or deleted directly. Discard the editing session instead.
404 The branch does not exist Branch '…' was not found.
409 force=false and the branch has commits the default branch does not Branch '…' is not fully merged. Use force=true to delete anyway.

The "fully merged" check is made against the default branch, not against whichever branch you merged into. A branch merged into staging but not into main still needs force=true.


Commit history#

GET /api/v1/branches/{branch}/commits?limit=50
GET /api/v1/branches/{branch}/commits?limit=50&cursor=NTA
Parameter Type Description
limit integer Commits per page, 1 to 100. Any other value, or no value, gives 10
cursor string The nextCursor of the previous page. Omit it for the first page. A cursor Atlas cannot read is treated as the first page rather than refused

The branch name may be HEAD, which reads the branch the repository is parked on.

200 OK

{
  "commits": [
    {
      "hash": "9f3c1ab2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
      "message": "Merge branch 'feature/sync-2026-11-04'",
      "author": "ErpImport",
      "date": "2026-11-04T09:12:44+00:00"
    }
  ],
  "nextCursor": "NTA"
}

Commits are newest first. nextCursor is omitted on the last page; its absence is the end of history. The cursor is an opaque position, so a commit that lands while you page shifts what the next page holds. For a feed read once that does not matter; for an audit copy, read the whole history in one pass and reconcile by hash.

author is the Git author name: the signed-in person's name, the client id of an application that authenticated with client credentials, or Anonymous when authentication is off. See Commit attribution.

404 when the branch does not exist.


Compare two branches#

GET /api/v1/branches/{branch}/compare?base=main

The branch in the path is the head, the work being reviewed. base is required and is the branch it is measured against, usually the default branch.

200 OK

{
  "aheadCount": 3,
  "behindCount": 0,
  "changedFiles": [
    { "path": "domains/plant-model/ontology.jsonld", "changeKind": "modified" },
    { "path": "domains/plant-model/instances/cmms-assets.map.json", "changeKind": "added" }
  ]
}
Field Type Description
aheadCount integer Commits on the head that the base does not have: what a merge would bring in
behindCount integer Commits on the base that the head does not have: how far the head has fallen behind
changedFiles[].path string Repository-relative path with forward slashes
changedFiles[].changeKind string One of added, modified, deleted, renamed

The file list is the difference between the point where the two branches diverged and the head. A change made only on the base after that point does not appear, so the list is exactly what a merge of the head into the base would touch.

404 when either branch does not exist; the detail names which one.

A behindCount above zero before a merge is a warning, not an error. The merge will still try, but the more the base has moved, the more likely the two sides touched the same file. Merging the base into the head first, and resolving there, keeps the eventual merge into the base clean.


Merge a branch#

POST /api/v1/branches/{branch}/merge
Content-Type: application/json
{ "target": "main" }

The branch in the path is the source, the branch being merged. target is the branch it merges into.

Atlas merges in a private working copy of the target, so a merge never races another write to the same branch: writes to one target are serialised. A successful merge always produces a merge commit, authored by the caller, with the message Git gives a merge. If the target already contains everything on the source, nothing is committed and the call still succeeds.

200 OK is returned for both outcomes. Read the body.

A clean merge, or one Atlas resolved itself:

{ "success": true, "hasConflicts": false }

A merge Atlas could not resolve:

{
  "success": false,
  "hasConflicts": true,
  "conflictFiles": ["domains/plant-model/ontology.jsonld"]
}
Field Type Description
success boolean Whether the target now contains the source
hasConflicts boolean Whether the merge stopped on conflicts
conflictFiles string[] The files Atlas could not merge. Omitted when the merge succeeded

How Atlas resolves overlapping edits. When both branches changed the same ontology file, Atlas merges the definitions each side added rather than giving up. It does the same for the record of which example libraries were imported. Where it cannot, it abandons the merge: the target branch does not move, nothing is half-applied, and the files are listed. Fix them on the source, or merge the target into the source first and resolve there, then merge again.

Status When Detail
400 Source and target are the same branch Cannot merge 'main' into itself.
404 The source or the target does not exist Branch '…' not found
500 Git itself failed A generic message; the Atlas log has the cause

A merge changes files, not compiled worlds. Every domain on the target whose files or imported libraries the merge touched now reports isStale: true from its world status, and connectors, dashboards and queries keep reading the world compiled before the merge until you recompile. See Compile and Query Workflow.


Read one commit#

Both routes take any reference Git accepts: a full or abbreviated hash, HEAD, or a branch name, encoded as above.

The files a commit changed#

GET /api/v1/commits/{sha}/files

200 OK

[
  { "path": "domains/plant-model/ontology.jsonld", "changeKind": "modified" },
  { "path": "domains/plant-model/instances/cmms-assets.map.json", "changeKind": "added" }
]

404 when the commit does not exist.

A file before and after#

GET /api/v1/commits/{sha}/diff?path=domains/plant-model/ontology.jsonld
GET /api/v1/commits/{sha}/diff?path=domains/plant-model/ontology.jsonld&base=main
Parameter Type Required Description
path string Yes Repository-relative path with forward slashes, exactly as the files route returned it. It is a query parameter because the path contains slashes
base string No Read the "before" content from this reference instead of the commit's parent. Pass the base branch and a head branch as {sha} to see a file as a merge would change it

200 OK

{
  "beforeContent": "{ \"@context\": …",
  "afterContent": "{ \"@context\": …"
}

Both are the whole file as text. An added file has an empty beforeContent; a deleted file has an empty afterContent. Computing the line diff is the caller's job; Atlas returns the two versions.


Errors on this surface#

All failures are application/problem+json with type, title, status and detail. The type tokens are the shared ones in the Atlas API Overview:

Status Type URI token On this surface
400 .../errors/invalid-request A branch name that breaks a rule, renaming the default branch, merging a branch into itself, a missing required parameter
403 .../errors/forbidden Deleting the default branch, or renaming or deleting a private draft branch
404 .../errors/not-found A branch, reference or commit that does not exist
409 .../errors/conflict A name in use, or deleting an unmerged branch without force
500 .../errors/internal-error Git failed. The detail is generic by design; the Atlas log has the command's output

There is no code field in the body. Branch on status and the type token, and show detail to a person.


A review workflow for an automated import#

The shape these routes are built for: every run of a synchronisation writes to its own branch, a person reviews the difference, approval is a merge, and the history is the audit trail.

  1. Create the run's branch from the trunk. POST /api/v1/branches with {"name": "sync/cmms-2026-11-04", "from": "main"}. Pass from every time.

  2. Write to that branch. Every ontology and instance-map route is branch-scoped, so the run's writes go to /api/v1/branches/sync%2Fcmms-2026-11-04/domains/{domain}/... and never touch main. A write that fails validation lands in the caller's own draft rather than on the branch, so check each write's outcome as the resource articles describe; the compare in the next step shows only what reached the branch.

  3. Build the review. GET /api/v1/branches/sync%2Fcmms-2026-11-04/compare?base=main gives the count of commits and the list of files. For each file, GET /api/v1/commits/sync%2Fcmms-2026-11-04/diff?path=…&base=main gives the two versions. Those are the record of what changed, and what a reviewer is asked to approve.

  4. On approval, merge. POST /api/v1/branches/sync%2Fcmms-2026-11-04/merge with {"target": "main"}. Read success. On a conflict, merge main into the run's branch, let the run reapply its changes on top, and ask for review again.

  5. Recompile the trunk. PUT /api/v1/branches/main/domains/{domain}/world for each domain the compare listed. Until then the trunk's compiled world is the one from before the merge.

  6. Keep or delete the branch. The merge commit on main records what was approved and by whom, so the branch is no longer needed. DELETE …?force=false succeeds because the branch is now fully merged into the default branch. A run that was rejected is deleted with force=true, or kept for reference.

  7. Rolling back. There is no revert route. The history on main shows which merge introduced a change, and undoing it is a new branch with the corrective change, reviewed and merged the same way. Nothing is ever rewritten.

Give the pipeline its own Pulse client so that its commits carry its name. A reviewer's approval is then visible in the history as a merge commit by the person who approved it, when the merge is made from their sign-in, or by the pipeline, when the pipeline merges on their behalf.


Troubleshooting#

Symptom Likely cause What to do
404 for a branch that the list shows The name contains a slash that was not encoded Encode / as %2F in the path segment
A branch created without from starts at an unexpected commit Git used the branch the repository was parked on Always pass from
DELETE returns 400 force was not sent Send force=true or force=false on every delete
409 not fully merged after a successful merge The merge target was not the default branch The check is against the default branch; use force=true
The merge returned 200 but main has not changed success was false Read conflictFiles, resolve on the source, merge again
The merge succeeded but queries return old data The target's world was not recompiled Recompile each affected domain on the target
The last page of history has no nextCursor That is the end of history Stop paging
A commit's author is Anonymous The caller sent no identity, or authentication is off Authenticate every write

esc
↑↓ navigate · ↵ open · ⌘K anywhere