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.
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
| 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
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
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.
-
Create the run's branch from the trunk.
POST /api/v1/brancheswith{"name": "sync/cmms-2026-11-04", "from": "main"}. Passfromevery time. -
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 touchmain. 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. -
Build the review.
GET /api/v1/branches/sync%2Fcmms-2026-11-04/compare?base=maingives the count of commits and the list of files. For each file,GET /api/v1/commits/sync%2Fcmms-2026-11-04/diff?path=…&base=maingives the two versions. Those are the record of what changed, and what a reviewer is asked to approve. -
On approval, merge.
POST /api/v1/branches/sync%2Fcmms-2026-11-04/mergewith{"target": "main"}. Readsuccess. On a conflict, mergemaininto the run's branch, let the run reapply its changes on top, and ask for review again. -
Recompile the trunk.
PUT /api/v1/branches/main/domains/{domain}/worldfor each domain the compare listed. Until then the trunk's compiled world is the one from before the merge. -
Keep or delete the branch. The merge commit on
mainrecords what was approved and by whom, so the branch is no longer needed.DELETE …?force=falsesucceeds because the branch is now fully merged into the default branch. A run that was rejected is deleted withforce=true, or kept for reference. -
Rolling back. There is no revert route. The history on
mainshows 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 |
Related articles#
- Branch Management: the same operations in the Atlas interface, draft sessions, and a team workflow.
- Calling the Atlas API from Your Own Application: registering a client and the first authenticated call.
- Atlas API Overview: the full route list, error format and pagination.
- Atlas API - Domains: world status,
isStale, and compile. - Compile and Query Workflow: compiling after a change and querying the result.
- Instance Maps Programming Guide: the build order for an import pipeline.