Library set API
Aspire name api-libraryset. This is the API the product exists for: the library itself, and the endpoint that
resolves it.
Every route is scoped to the organization in the caller's JWT.
Tag groups
| Method | Path | Permission |
|---|---|---|
GET | /taggroups | TagGroupList |
GET | /taggroups/{id} | TagGroupRead |
POST | /taggroups | TagGroupCreate |
PUT | /taggroups/{id} | TagGroupUpdate |
DELETE | /taggroups/{id} | TagGroupDelete |
{
"id": 1,
"version": 2,
"name": "Environment",
"description": "Where the code is running",
"tags": [
{ "id": 1, "name": "Production", "tagGroupId": 1 },
{ "id": 2, "name": "Staging", "tagGroupId": 1 }
],
"createdBy": { "id": "…", "name": "Neil Docherty" },
"createdById": "…",
"createdAt": "2026-08-01T10:00:00Z",
"updatedBy": { "id": "…", "name": "Neil Docherty" },
"updatedById": "…",
"updatedAt": "2026-08-04T11:30:00Z"
}
Deleting a group cascades to its tags, and the join rows to values cascade from either side.
Tags
| Method | Path | Permission |
|---|---|---|
GET | /tags | TagList |
GET | /tags/{id} | TagRead |
POST | /tags | TagCreate |
PUT | /tags/{id} | TagUpdate |
DELETE | /tags/{id} | TagDelete |
{ "name": "Production", "tagGroupId": 1 }
Library sets
Called variable sets in the API, library sets in the UI and import file.
| Method | Path | Permission |
|---|---|---|
GET | /variablesets | VariableList |
GET | /variablesets/{id} | VariableRead |
POST | /variablesets | VariableCreate |
PUT | /variablesets/{id} | VariableUpdate |
DELETE | /variablesets/{id} | VariableDelete |
{ "name": "orders-service", "description": "Config for the orders service" }
GET /variablesets/{id} returns the set with its variables and their values, with sensitive values withheld
unless the caller holds VariableReadSecrets. Deleting a set cascades to its variables and their values.
Variables
| Method | Path | Permission |
|---|---|---|
GET | /variables | VariableList |
GET | /variables/{id} | VariableRead |
POST | /variables | VariableCreate |
PUT | /variables/{id} | VariableUpdate |
DELETE | /variables/{id} | VariableDelete |
{
"name": "DatabaseConnection",
"variableSetId": 1,
"values": [
{ "value": "Server=localhost;Database=orders", "sensitive": false, "tags": [] },
{ "value": "Server=prod.internal;Database=orders", "sensitive": false, "tags": ["Environment/Production"] },
{ "value": "sk-live-…", "sensitive": true, "tags": ["Environment/Production", "Region/EU"] }
]
}
tags accepts "Group/Tag" strings or numeric tag IDs, mixed freely in one list. A tag that names nothing is
skipped rather than refused.
Two values of the same variable carrying the same tag combination are refused with 409 Conflict.
{
"id": 1,
"version": 1,
"name": "DatabaseConnection",
"variableSetId": 1,
"values": [
{
"id": 1,
"value": "Server=localhost;Database=orders",
"sensitive": false,
"hint": null,
"hash": null,
"tags": []
}
],
"createdAt": "…", "createdBy": "…", "updatedAt": "…", "updatedBy": "…"
}
Values come back ordered by ID, so a variable does not list its values differently between reads.
Variable values
The consumption endpoints. Both take the same parameters and run the same resolution; they differ in what they hand back.
| Method | Path | Permission |
|---|---|---|
GET | /variablevalues | VariableRead |
GET | /variablevalues/preview | VariableRead |
| Parameter | |
|---|---|
set | Repeated. At least one required — none is 400. A set that does not exist is 404. |
tag | Repeated, Group/Tag form. Optional. |
GET /variablevalues?set=1&set=2&tag=Environment/Production&tag=Region/EU
{
"total": 2,
"items": {
"DatabaseConnection": "Server=eu-prod.internal;Database=orders",
"LogLevel": "Warning"
}
}
{
"total": 1,
"items": {
"DatabaseConnection": {
"id": 3,
"value": "Server=eu-prod.internal;Database=orders",
"sensitive": false,
"hint": null,
"hash": null,
"tags": [
{ "id": 1, "name": "Production", "tagGroupId": 1 },
{ "id": 3, "name": "EU", "tagGroupId": 2 }
]
}
}
}
See Resolution for the matching rule and how each endpoint treats secrets.
Import
| Method | Path | Permission |
|---|---|---|
POST | /import | BatchProcessingImport (and the fifteen it depends on) |
| Parameter | Default | |
|---|---|---|
replaceExisting | false | true empties the organization's library first |
A whole library in one request, one transaction and one save. The file is the contract, so the payload is shaped after the file rather than after the models: it says "library sets", and nothing in it carries an ID. Names are the only identity a file has, so names are what an import matches on.
{
"tagGroups": {
"Environment": ["Production", "Staging"],
"Region": ["EU", "US"]
},
"librarySets": {
"orders-service": {
"DatabaseConnection": [
{ "value": "Server=localhost;Database=orders", "sensitive": false, "scopes": [] },
{ "value": "Server=prod.internal;Database=orders", "sensitive": false, "scopes": ["Environment/Production"] }
],
"ApiToken": [
{ "value": "sk-live-…", "sensitive": true, "scopes": ["Environment/Production"] }
]
}
}
}
{
"replaced": false,
"tagGroupsCreated": 2, "tagGroupsSkipped": 0,
"tagsCreated": 4, "tagsSkipped": 0,
"librarySetsCreated": 1, "librarySetsSkipped": 0,
"variablesCreated": 2, "variablesSkipped": 0,
"valuesCreated": 3,
"tagGroupsDeleted": 0, "tagsDeleted": 0, "librarySetsDeleted": 0,
"variablesDeleted": 0, "valuesDeleted": 0,
"notes": ["Tag group 'Environment' already existed and was left alone"]
}
A rejected file is rolled back, not merely uncommitted — so a rejected replacing import leaves the emptied library exactly as it found it.
Auditing is one entry for the whole import, carrying the counts without the notes. Re-importing an unchanged file is the ordinary repeat case and produces a note per entity left alone, which is worth telling the caller and not worth shipping to another service as a record of nothing having changed.
See Importing a library.