Skip to main content

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​

MethodPathPermission
GET/taggroupsTagGroupList
GET/taggroups/{id}TagGroupRead
POST/taggroupsTagGroupCreate
PUT/taggroups/{id}TagGroupUpdate
DELETE/taggroups/{id}TagGroupDelete
GET /taggroups/1
{
"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​

MethodPathPermission
GET/tagsTagList
GET/tags/{id}TagRead
POST/tagsTagCreate
PUT/tags/{id}TagUpdate
DELETE/tags/{id}TagDelete
POST /tags
{ "name": "Production", "tagGroupId": 1 }

Library sets​

Called variable sets in the API, library sets in the UI and import file.

MethodPathPermissionCredits
GET/variablesetsVariableListFree
GET/variablesets/{id}VariableRead1
POST/variablesetsVariableCreateFree
PUT/variablesets/{id}VariableUpdateFree
DELETE/variablesets/{id}VariableDeleteFree
POST /variablesets
{ "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​

MethodPathPermissionCredits
GET/variablesVariableListFree
GET/variables/{id}VariableRead1
POST/variablesVariableCreateFree
PUT/variables/{id}VariableUpdateFree
DELETE/variables/{id}VariableDeleteFree

GET /variables/{id} is billable — see Credits. An organization without enough credits is refused with 429 Too Many Requests. Listing variables is free.

POST /variables
{
"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.

200 OK
{
"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.

MethodPathPermissionCredits
GET/variablevaluesVariableRead10
GET/variablevalues/previewVariableRead10

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
setRepeated. At least one required — none is 400. A set that does not exist is 404.
tagRepeated, Group/Tag form. Optional.
GET /variablevalues?set=1&set=2&tag=Environment/Production&tag=Region/EU
/variablevalues
{
"total": 2,
"items": {
"DatabaseConnection": "Server=eu-prod.internal;Database=orders",
"LogLevel": "Warning"
}
}
/variablevalues/preview
{
"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​

MethodPathPermission
POST/importBatchProcessingImport (and the sixteen it depends on)
ParameterDefault
replaceExistingfalsetrue 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.

POST /import
{
"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.

200 OK
{
"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​

MethodPathPermissionCredits
GET/exportBatchProcessingExport (and the seven it depends on)1 per library set exported
Parameter
tagGroupIdRepeatedThe tag groups to write, by ID
variableSetIdRepeatedThe library sets to write, by ID
secretExportOptionBlank by defaultValue, 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​

MethodPathPermission
GET/organizations/{id}/certificatesGlobalOrganizationRead (global)
POST/organizations/{id}/certificates/generateGlobalOrganizationCertificateManage (global)
POST/organizations/{id}/certificates/uploadGlobalOrganizationCertificateManage (global)
POST/organizations/{id}/certificates/rotateGlobalOrganizationCertificateManage (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.

POST /organizations/{id}/certificates/generate
{ "slot": "Primary" }
POST /organizations/{id}/certificates/upload
{
"slot": "Replacement",
"certificatePem": "-----BEGIN CERTIFICATE-----…",
"privateKeyPem": "-----BEGIN PRIVATE KEY-----…"
}
200 OK — any of the above, or GET
{
"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.