Two HTTP surfaces, mounted on the same SGApps.IO host:
| Surface | Prefix | Auth | Audience |
|---|---|---|---|
| Read API | /site-builder/api/erp-config/ | PRIVATE-TOKEN header | External ERPs |
| Admin API | /site-builder/api/ | Session cookie | The webapp UI |
GitLab v4-compatible shape — existing GitLab API clients work unmodified.
<div class="endpoint-table">
| Method | Endpoint | Returns |
|---|---|---|
GET | /site-builder/api/erp-config/projects/{repoId}/repository/branches?search= | [{name, commit:{short_id}}] |
GET | /site-builder/api/erp-config/projects/{repoId}/repository/tree?ref=&path=&recursive=1 | [{id, name, type, path, mode}] |
GET | /site-builder/api/erp-config/projects/{repoId}/repository/files/{rawencoded-path}/raw?ref= | Raw file body |
GET | /site-builder/api/erp-config/health | {status:"ok", time} |
curl -H "PRIVATE-TOKEN: abc...xyz" \
"https://sgapps.io/site-builder/api/erp-config/projects/b2b-cnc.erp-config/repository/files/operations.config.json/raw?ref=master"curl -H "PRIVATE-TOKEN: abc...xyz" \
"https://sgapps.io/site-builder/api/erp-config/projects/b2b-cnc.erp-config/repository/branches"Returns (GitLab v4 compatible shape):
[
{
"name": "main",
"commit": {
"id": "ac800ee0ab12cd34ef56789012345678901abcde",
"short_id": "ac800ee0"
},
"default": true,
"protected": false,
"merged": false
}
]The default: true flag is set on the first branch matching this priority:
master, if it existsmain, if it existsGitLab API clients that fall back to "the default branch" when no ref isspecified will follow this resolution. Explicit ?ref= always wins.
refFiles live under a specific branch. A 404 File Not Found on/files/{path}/raw?ref=master when the file actually exists almostalways means the project uses main (or a custom branch name) instead.List branches first to discover the right ref, or rely on default: true.
All endpoints are gated by the SGApps session cookie. {owner} is implicit(taken from session.user._id); requests trying to touch another user'sprojects return 403.
<div class="endpoint-table">
| Method | Endpoint | Body / Query | Description |
|---|---|---|---|
GET | /site-builder/api/projects | — | List your projects |
POST | /site-builder/api/projects | {name, type} | Create new project |
GET | /site-builder/api/projects/{repoId}/branches | — | List branches |
POST | /site-builder/api/projects/{repoId}/branches | {name} | Create branch (empty folder) |
GET | /site-builder/api/projects/{repoId}/branches/{ref}/tree?path=&recursive=1 | — | Tree listing |
GET | /site-builder/api/projects/{repoId}/branches/{ref}/files/{path} | — | Raw file body |
PUT | /site-builder/api/projects/{repoId}/branches/{ref}/files/{path} | Raw body (octet-stream) | Save file, snapshot first |
GET | /site-builder/api/projects/{repoId}/branches/{ref}/history | — | List snapshots |
GET | /site-builder/api/projects/{repoId}/branches/{ref}/snapshots/{version}/tree?path=&recursive=1 | — | List files inside a snapshot (read-only) |
GET | /site-builder/api/projects/{repoId}/branches/{ref}/snapshots/{version}/files/{path} | — | Raw file body inside a snapshot |
POST | /site-builder/api/projects/{repoId}/branches/{ref}/rollback?to=v{NNNN} | — | Restore snapshot (creates pre-rollback snapshot too) |
GET | /site-builder/api/tokens | — | List your tokens (suffix only — full value never re-exposed) |
POST | /site-builder/api/tokens | {name, repos[], expires_at, fingerprint_required} | Create token (returns tokenString once) |
PATCH | /site-builder/api/tokens/{tokenId} | partial body | Update repos / expiry / fingerprint flag |
DELETE | /site-builder/api/tokens/{tokenId} | — | Revoke token |
PRIVATE-TOKEN (external read API)Set on every request:
PRIVATE-TOKEN: <64-char hex token>The server resolves the token to a record `{owner, repos[], expires_at,fingerprint_required}. Expired tokens reject with 401. The repos[]`field controls project scope:
repos: [] (empty array) — wildcard: the token can access any
project under its owner's folder. Owner-scoping still applies — a
wildcard token can never reach another user's projects, just any of
its own owner's.repos: ["b2b-cnc.erp-config"] — strict allowlist by exact
repoId. Requests for a repoId not in the list reject with 403.Choose strict allowlist for production integrations (principle of leastprivilege); wildcard is convenient for one-operator setups or short-livedexploration tokens where the owner trusts the holder with everything.
X-Instance-Id fingerprint bindingOptional defense against token leaks. When a token has `fingerprint_required:true`:
X-Instance-Id: <stable-id> along
with PRIVATE-TOKEN. The server stores the fingerprint.X-Instance-Id must be sent. The
server compares with crypto.timingSafeEqual. Mismatches reject with
401 FINGERPRINT_MISMATCH.A leaked token is then useless without the matching fingerprint. Pick afingerprint that's stable per instance (machine UUID, container ID, etc.)but unguessable from outside.
Beyond the auth surfaces above, file-level access in the SGApps virtualfilesystem (where projects live) is gated by these roles. They're set onuser accounts via the user-management UI and stack additively — grantinga higher-privilege role doesn't revoke a lower one.
| Role | Scope | Grants |
|---|---|---|
ROLE_SITEBUILDER_USER | own folder /apps/site-builder/user/{me}/ | full RW (idempotent with the default owner-access today; becomes a gating role if owner-default is removed) |
ROLE_SITEBUILDER_USER__{prefix} | projects with repoId matching {prefix}, {prefix}.{type}, or {prefix}/... — in any user's folder | full RW (cross-user collaboration role) |
ROLE_SITEBUILDER_EDITOR__PUBLIC | /apps/site-builder/public/ | write/remove (read is world-public) |
ROLE_SITEBUILDER_EDITOR__READONLY | entire /apps/site-builder/* tree, cross-user | read + list (audit / monitoring) |
ROLE_SITEBUILDER_EDITOR__ROOT | entire /apps/site-builder/* tree, cross-user | full RW + metadata + remove (platform admin) |
ROLE_SITEBUILDER_USER__b2b-cnc granted on user Alice gives Aliceedit access to b2b-cnc.erp-config, b2b-cnc.fiscal-mev, etc. inside thefolder of any user that hosts such projects. Use full repoIds(ROLE_SITEBUILDER_USER__b2b-cnc.erp-config) to restrict to one specificproject instead of the whole group.The webapp authenticates via the standard SGApps session cookie set by/user/login. There's nothing the API has to do — the existingSessionManager middleware drops req.session.data.loginData.local intothe request before the handler runs.
The read API exposes commit.short_id per branch in the branches listingresponse. It's the first 8 chars of `sha256(concat sortat după path de"{path}|{size}|{md5}\n")` over all files in the live branch.
| Property | Value |
|---|---|
| Stability | Identical for unchanged content (deterministic) |
| Invalidation | Recomputed on next read after a PUT |
| Cost | One GridFS list per branch — no content reads |
short_id, and skip re-fetching files until the short_idchanges.| Status | When | Body |
|---|---|---|
400 | Malformed path / branch name / missing required field | {message} |
401 | Missing / invalid / expired token; session not logged in | {message} |
403 | Token doesn't cover requested repo; owner mismatch on admin route | {message} |
404 | Project / branch / file / snapshot not found | {message} |
409 | (Reserved for future optimistic-lock writes) | {message} |
500 | Storage error | {message} |
message field is human-readable but stableenough to switch on (e.g. 401 FINGERPRINT_MISMATCH for the fingerprint case).