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 | Credits |
|---|---|---|---|
GET | /variablesets | VariableList | Free |
GET | /variablesets/{id} | VariableRead | 1 |
POST | /variablesets | VariableCreate | Free |
PUT | /variablesets/{id} | VariableUpdate | Free |
DELETE | /variablesets/{id} | VariableDelete | Free |
{ "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 | Credits |
|---|---|---|---|
GET | /variables | VariableList | Free |
GET | /variables/{id} | VariableRead | 1 |
POST | /variables | VariableCreate | Free |
PUT | /variables/{id} | VariableUpdate | Free |
DELETE | /variables/{id} | VariableDelete | Free |
GET /variables/{id} is billable — see Credits. An organization without enough credits is
refused with 429 Too Many Requests. Listing variables is free.
{
"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 | Credits |
|---|---|---|---|
GET | /variablevalues | VariableRead | 10 |
GET | /variablevalues/preview | VariableRead | 10 |
Both are billed per call, not per value returned, and only when they succeed. If the
organization cannot afford the call it is refused with 429 Too Many Requests and a plain-text body such as
Insufficient credits: 3 available, 10 required. Costs are configurable — see
CreditCosts.
| 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 sixteen 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": {
"common": {
"variables": {
"LogLevel": [{ "value": "Warning", "sensitive": false, "scopes": [] }]
}
},
"orders-service": {
"slug": "orders-service",
"description": "Config for the orders service",
"parent": "common",
"variables": {
"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"] }
]
}
}
}
}
slug, description and parent are all optional, and only apply when a set is being created — matching an
existing set by name still only adds the variables it doesn't already hold. parent names another set's
slug, either one already in the organization or one this same file also defines. See
Importing a library for the details.
{
"replaced": false,
"tagGroupsCreated": 2, "tagGroupsSkipped": 0,
"tagsCreated": 4, "tagsSkipped": 0,
"librarySetsCreated": 2, "librarySetsSkipped": 0,
"variablesCreated": 3, "variablesSkipped": 0,
"valuesCreated": 4,
"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.
Export
| Method | Path | Permission | Credits |
|---|---|---|---|
GET | /export | BatchProcessingExport (and the seven it depends on) | 1 per library set exported |
| Parameter | ||
|---|---|---|
tagGroupId | Repeated | The tag groups to write, by ID |
variableSetId | Repeated | The library sets to write, by ID |
secretExportOption | Blank by default | Value, Hint, Hash or Blank — what a sensitive value writes in place of itself |
The mirror of import: the response is a POST /import body, naming exactly the tag groups and library sets
asked for. At least one of tagGroupId/variableSetId is required — neither is 400; an ID that does not
exist in the organization is 404.
secretExportOption=Value additionally requires VariableReadSecrets — refused with 403 without it, rather
than silently downgraded to one of the other options. Every other option needs nothing beyond
BatchProcessingExport.
An export costs one credit per library set exported (variableSetId); tag groups cost
nothing. The organization must have enough credits for every library set named — otherwise the whole request is
refused with 429 Too Many Requests before anything is read, never a partial file. Nothing is charged for a
request that fails.
GET /export?tagGroupId=1&variableSetId=2&secretExportOption=Hint
See Exporting a library for the full shape and what each secret option produces.
See Importing a library.
Certificates
| Method | Path | Permission |
|---|---|---|
GET | /organizations/{id}/certificates | GlobalOrganizationRead (global) |
POST | /organizations/{id}/certificates/generate | GlobalOrganizationCertificateManage (global) |
POST | /organizations/{id}/certificates/upload | GlobalOrganizationCertificateManage (global) |
POST | /organizations/{id}/certificates/rotate | GlobalOrganizationCertificateManage (global) |
DELETE | /organizations/{id}/certificates/{slot} | GlobalOrganizationCertificateManage (global) |
Held here rather than on the account API, next to the sensitive Variable Values they protect - see Client certificates for what each does and how the private key is kept out of reach of the API itself.
{ "slot": "Primary" }
{
"slot": "Replacement",
"certificatePem": "-----BEGIN CERTIFICATE-----…",
"privateKeyPem": "-----BEGIN PRIVATE KEY-----…"
}
{
"id": "3f52a18b-…",
"slot": "Primary",
"source": "Generated",
"thumbprint": "A1B2C3…",
"validFrom": "2026-08-19T00:00:00Z",
"validTo": "2027-08-19T00:00:00Z",
"createdAt": "2026-08-19T00:00:00Z",
"updatedAt": "2026-08-19T00:00:00Z"
}
Nothing above ever carries a private key, a PFX, or the certificate's own bytes — this is the whole of what
any of these endpoints hands back, whichever slot or action is asked for. rotate takes no body and swaps
which slot each existing certificate occupies, then re-encrypts every sensitive Variable Value this
organization holds against the new primary in the same request; it refuses with 400 unless both slots are
already filled.