Skip to main content
Last updated: July 31, 2026tiCrypt

tiCrypt REST API

Base URL is the backend domain

The base URL is the backend domain of your deployment — the same host that serves the tiCrypt web interface. There is no separate API host: the REST API is served by the backend itself, under /api.

It is the value of global.backendDomain in ticrypt.yml, set during installation. See the Install Guide to confirm yours.

Examples below use https://ticrypt.yourinstitution.edu. Substitute your own backend domain.

Version requirement

Everything on this page works in full on tiCrypt backend 3.14.1 or later.

Anything added in that release carries a v3.14.1+ badge, on the route heading and in the route summary tables. Unbadged routes work on earlier versions. Three things need 3.14.1:

  • Multi-type tokens — one token can hold several types instead of exactly one.
  • Ten new routes, listed under New in 3.14.1.
  • New fields on existing response structures, marked // As of 3.14.1 in the interface definitions.

The System Snapshot script depends on all three, and reaches full coverage only on 3.14.1.

Authentication

The tiCrypt API uses token-based authentication. Each token carries one or more resource types, which determine which routes it can reach. Tokens are generated through the tiCrypt administrative interface.

Token Types

Each type grants its own set of routes:

Token TypeAvailable Routes
DriveDrive Routes
VMVM Routes
TeamTeam Routes
ProjectProject Routes
UserUser Routes

Security Routes are reached with a Project token.

Tokens are created in the tiCrypt front end under Management → Miscellaneous → API Keys. See API Keys for the click-path.

Multi-Type Tokens v3.14.1+

Before 3.14.1 a token carried exactly one type, so a request outside that type was rejected. Work spanning resource types meant one token per type, each with its own expiration and rotation.

As of 3.14.1 a token carries any combination of types. Select every type and one token reaches every route on this page, which is what the System Snapshot script expects. Pair it with readOnly: true for a token that reads the whole system and changes nothing.

Single-type tokens are unaffected. Tokens created before the upgrade keep working exactly as they did.

Passing Tokens

Tokens can be passed in one of two ways:

MethodExample
URL parameter/api/drives?token=YOUR_TOKEN
Cookie headerCookie: TC_TOKEN=YOUR_TOKEN
Token Precedence

If both a URL parameter and a cookie are provided, the URL parameter takes precedence and the cookie is ignored.

Read-Only Tokens (as of ver. 3.13.8)

Tokens can be created as read-only by passing "readOnly": true in the token creation request body. A read-only token is restricted to read and list operations. Any request that mutates state is rejected with an error.

If readOnly is omitted or set to false at creation time, the token behaves as a full-access token for its type.

When fetching or creating a token, the readOnly field is included in the response:

type TokenType = "Drive" | "VM" | "Team" | "Project" | "User";

// Returned when listing or fetching tokens.
interface APITokenInfo {
id: string; // UUID of the token, for management
tokenTypes: TokenType[];// As of 3.14.1. Every type the token can reach
tokenType: TokenType; // Deprecated as of 3.14.1. Equals tokenTypes[0]
description?: string; // Optional text description
created: number; // Epoch time in millis
expires: number; // Epoch time in millis
lastUse?: number; // Epoch time in millis, last request made with it
owner: string; // ID of the user who created the token
readOnly: boolean; // Whether the token is restricted to read routes
}

// Returned once, at creation.
interface APITokenCreateResponse {
id: string;
tokenTypes: TokenType[];// As of 3.14.1
tokenType: TokenType; // Deprecated as of 3.14.1. Equals tokenTypes[0]
token: string; // The token value itself
readOnly: boolean; // Resolved value (false if not specified)
}
Deprecated Field

tokenType is retained so clients written against 3.14.0 and earlier keep working. It reports only the first of a multi-type token's types, so a token with all five types still reads as "Drive". Read tokenTypes instead.

caution

The token value is returned once and only once, at creation. It is never stored by the server. If lost, delete the token and create a new one.

Routes accessible with a read-only token:

RouteMethodDescription
/api/drivesGETGet Drives
/api/realmsGETGet Realms v3.14.1+
/api/vms/imagesGETGet VM Images
/api/vms/bricksGETGet Bricks v3.14.1+
/api/vms/<realmID>/activeGETGet Active VMs for Realm
/api/vms/<realmID>/configsGETGet VM Configs
/api/external-serversGETGet External Servers v3.14.1+
/api/teamsGETGet Teams
/api/teams/quotasGETGet Team Quotas
/api/teams/usageGETGet Team Usage v3.14.1+
/api/projectsGETGet Projects
/api/projects/<projectID>GETGet Project
/api/projects/membershipsGETGet Project Memberships v3.14.1+
/api/projects/usageGETGet Project Usage v3.14.1+
/api/security-requirementsGETGet Security Requirements v3.14.1+
/api/security-levelsGETGet Security Levels v3.14.1+
/api/usersGETList Users
/api/users/certificationsGETGet User Certifications v3.14.1+
/api/users/managed-objectsGETGet Managed Objects v3.14.1+

Routes blocked for read-only tokens:

RouteMethodDescription
/api/vms/imagesPOSTCreate VM Image
/api/vms/bricksPATCHEdit Brick
/api/teamsPOSTCreate Team
/api/teams/quotasPATCHEdit Team Quotas
/api/users/disablePOSTDeactivate Users

Error Handling

When a request fails, the API returns an appropriate HTTP status code with an error message in the response body.

Common Status Codes

StatusMeaning
200 OKRequest succeeded
400 Bad RequestMalformed request body or missing required fields
401 UnauthorizedMissing or invalid token
403 ForbiddenToken does not have permission for this operation (e.g., read-only token on a mutating route, or wrong token type)
404 Not FoundThe requested resource does not exist
422 Unprocessable EntityRequest body is valid JSON but contains invalid values
500 Internal Server ErrorUnexpected server error
Pagination

List endpoints return all results in a single response. There is no pagination. For large datasets, filter on the client side.


New in 3.14.1

Ten routes are added in 3.14.1. Each is documented in full in its own section below, and each carries a v3.14.1+ badge there.

RouteMethodToken typeSection
/api/realmsGETVMGet Realms
/api/vms/bricksGETVMGet Bricks
/api/external-serversGETVMGet External Servers
/api/teams/usageGETTeamGet Team Usage
/api/projects/membershipsGETProjectGet Project Memberships
/api/projects/usageGETProjectGet Project Usage
/api/security-requirementsGETProjectGet Security Requirements
/api/security-levelsGETProjectGet Security Levels
/api/users/certificationsGETUserGet User Certifications
/api/users/managed-objectsGETUserGet Managed Objects

Alongside them, 3.14.1 adds multi-type tokens and new fields on TeamMemberInfo, DriveInfo, ImageInfo, and BrickInfo, each marked // As of 3.14.1 where it is defined.


Drive Routes

Route Summary

RouteMethodDescription
/api/drivesGETGet Drives

Get Drives

GET /api/drives

Token type: Drive

Lists all drives on the system.

Request body: None

Response:

StatusDescription
200 OKReturns an array of DriveInfo objects
401 UnauthorizedInvalid or missing token
interface DriveInfo {
id: string;
name: string;
owner: string;
capacity: number; // Total capacity in bytes
format: string; // ext4, ntfs, etc.
noBackup?: boolean; // Whether the drive has backup disabled (Deprecated)
settings?: object; // Frontend-specific JSON settings
writer?: string; // ID of any VM attached as read-write
readers: string[]; // IDs of VMs attached as read-only
created: number; // Epoch time in millis
changed?: number; // Epoch time in millis, last modified
attached?: number; // Epoch time in millis, last attached
writable?: boolean;
poolID?: string; // Libvirt pool ID, if not in default pool
team?: string;
project?: string;
realmID: string;
diskType?: string; // qcow2, raw, etc.
cache?: string;
io?: string;
state: string; // Ready | ReadOnly | ReadWrite | Transferring | Initializing
stateChange?: number; // Epoch time in millis
hasSnapshot: boolean;
sizeOnDisk: number; // As of 3.14.1. Bytes actually consumed
keys: number; // As of 3.14.1. Number of keys on the drive
snapshotPool?: string; // As of 3.14.1. Pool holding the snapshots
hasMessage: boolean; // As of 3.14.1. Whether a message is attached
}
Deprecated Field

The noBackup field is deprecated and defaults to true. It will be removed in a future version.

Provisioned size vs. consumed size v3.14.1+

capacity is the size the drive was provisioned at. sizeOnDisk is what it actually consumes, which is what capacity planning needs and what no field exposed before 3.14.1.

The two are independent: sizeOnDisk is not bounded by capacity in practice, because snapshots and metadata are counted alongside the drive itself. Do not infer one from the other.

Example response:

[
{
"id": "8cd6c832-d5db-45cf-9de2-b2c2f1a9b107",
"name": "Research Data Vol 1",
"owner": "60eeec74-37db-4f69-bb46-bf4ca4f69e03",
"capacity": 107374182400,
"format": "ext4",
"readers": [],
"created": 1747838038956,
"writable": true,
"realmID": "libvirt",
"diskType": "qcow2",
"state": "Ready",
"hasSnapshot": false
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/drives?token=YOUR_TOKEN'

VM Routes

Route Summary

RouteMethodDescription
/api/realmsGETGet Realms v3.14.1+
/api/vms/imagesGETGet VM Images
/api/vms/imagesPOSTCreate VM Image
/api/vms/bricksGETGet Bricks (Hardware Setups) v3.14.1+
/api/vms/bricksPATCHEdit Brick (Hardware Setup)
/api/vms/<realmID>/activeGETGet Active VMs for Realm
/api/vms/<realmID>/configsGETGet VM Configs
/api/external-serversGETGet External Servers v3.14.1+

Get Realms v3.14.1+

GET /api/realms

Token type: VM

Lists the virtualization realms configured on the system. Every route with a <realmID> path segment takes one of these IDs.

Request body: None

Response:

StatusDescription
200 OKReturns an array of RealmInfo objects
401 UnauthorizedInvalid or missing token
interface RealmInfo {
id: string; // Used as <realmID> in VM route paths
name: string; // Display name
type: string; // Virtualization backend
}

Example response:

[
{
"id": "libvirt",
"name": "Libvirt",
"type": "libvirt"
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/realms?token=YOUR_TOKEN'

Get VM Images

GET /api/vms/images

Token type: VM

Lists all images for the specified realm.

Request body:

FieldTypeRequiredDescription
realmstringYesRealm ID to list images for

Response:

StatusDescription
200 OKReturns an array of ImageInfo objects
400 Bad RequestMissing realm field
401 UnauthorizedInvalid or missing token
interface ImageInfo {
id: string;
name?: string;
description?: string;
size?: number; // Size in bytes
imageType?: string; // "windows" | "linux"
info: LibvirtExtraInfo;
created?: number; // Epoch time in millis
modified?: number; // Epoch time in millis
version?: number; // Empty if base image (ver. 0)
parent?: string; // Empty if base image
limit?: number; // Max concurrent VMs (undefined = no limit)
settings?: object; // Frontend-only settings
secureBoot: boolean; // As of 3.14.1. Secure Boot required
}

LibvirtExtraInfo

interface LibvirtExtraInfo {
pool: string; // Pool ID
volume: string; // Volume name in pool (e.g., "my-image.img")
deviceBusType: "scsi" | "virtio";
}

Example response:

[
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"name": "Rocky 8 Production",
"description": "Rocky Linux 8 base image with tiCrypt VM controller",
"size": 5368709120,
"imageType": "linux",
"info": {
"pool": "ticrypt-vm-drives",
"volume": "rocky8-prod.qcow2",
"deviceBusType": "virtio"
},
"created": 1740000000000,
"modified": 1747000000000,
"version": 0
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/vms/images?realm=libvirt&token=YOUR_TOKEN'

Get VM Configs

GET /api/vms/<realmID>/configs

Token type: VM

Returns all VM configurations for the specified realm.

Request body: None

Response:

StatusDescription
200 OKReturns an array of VMConfigInfo objects
401 UnauthorizedInvalid or missing token
interface VMConfigInfo {
id: string;
vmID?: string; // UUID of spawned VM
state: "Stopped" | "Starting" | "Running" | "Suspended" | "Stopping";
owners: string[];
name: string;
description: string;
brick: string; // Hardware Setup ID
team?: string;
project?: string;
rwDrives: string[]; // Read-write drive IDs
roDrives: string[]; // Read-only drive IDs
mac?: string;
host?: string; // Pinned host
controllerID?: string;
driveSlots: { [key: string]: number };
macOptions: string[];
settings?: object;
lastPing?: number; // Epoch time in millis; last time the VM pinged the backend
lastLaunched?: number; // Epoch time in millis; last time this VM config was launched
}
note

lastLaunched and lastPing are optional. For a configuration that has never been launched, both fields are absent; lastLaunched is set on the first launch and lastPing once the spawned VM first contacts the backend.

Example response:

[
{
"id": "7f1b326d-0eef-4875-9deb-d820ace7a372",
"brick": "dd86080c-062a-4aba-889a-dbf64624e917",
"description": "",
"driveSlots": {},
"lastLaunched": 1747838038956,
"lastPing": 1747955324629,
"mac": "9c:75:16:ee:b5:3e",
"macOptions": [],
"name": "July08-rocky",
"owners": ["8e5789ae-da9f-48c1-8e63-ea989a4249a0"],
"project": "proj|38fa1dae-748e-45ae-8a1f-c799a4840a18",
"roDrives": [],
"rwDrives": ["8cd6c832-d5db-45cf-9de2-b2c2f1a9b107"],
"state": "Stopped",
"team": "team|2d48a972-4dd0-44e6-803f-956dbc56503c",
"vmID": "3a4be5cf-2e3c-43ff-879e-2b62e8665dd6"
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/vms/libvirt/configs?token=YOUR_TOKEN'

Get Active VMs for Realm

GET /api/vms/<realmID>/active

Token type: VM

Returns all active (running) VMs for the specified realm.

Request body: None

Response:

StatusDescription
200 OKReturns an array of VirtualMachine objects
401 UnauthorizedInvalid or missing token
interface VirtualMachine {
id: string;
name: string;
owner: string;
brick: string; // Hardware Setup ID
active: boolean;
pubkey?: PublicKey; // RSA public key components
drives: { [key: string]: string }; // Device name → drive ID (e.g., "vda" → "driveID")
ip: string; // Host address
team?: string;
project?: string;
protocolVer: number;
authedUsers: string[]; // All authorized users (excluding owner)
managers: string[]; // Users who can authorize others (subset of authedUsers)
realmID: string;
resources?: HostUsage;
ownerType?: "User" | "VM";
vpn: HostAndPort;
ssh: HostAndPort;
lastPing?: number; // Epoch time in millis
start?: number; // Epoch time in millis
shutDown?: number; // Epoch time in millis
config?: string; // VM Config ID
vmcInfo: VMCInfo[];
systemInfo?: object;
}

interface PublicKey {
modulus: string; // Hex-encoded RSA modulus
publicExponent: string; // Hex-encoded public exponent (e.g., "10001")
}

interface HostUsage {
vms: number;
vcpus: number;
memory: number;
devices: { [deviceType: string]: number };
}

interface HostAndPort {
host: string;
port: number;
}

interface VMCInfo {
timestamp: number;
info: object;
}

Example response:

[
{
"id": "13154348-ed6c-48fc-bf6c-9591bc974738",
"name": "Test VM",
"owner": "60eeec74-37db-4f69-bb46-bf4ca4f69e03",
"brick": "dd86080c-062a-4aba-889a-dbf64624e917",
"active": true,
"pubkey": {
"modulus": "c20549a1563674f0795736e95ac0ef793fb3a586...",
"publicExponent": "10001"
},
"drives": {
"vdb": "94d13381-4720-4318-a594-d99eeacd151a",
"vdc": "ef71c696-360d-46a0-8e15-93ca6a635e28"
},
"ip": "192.168.122.80",
"authedUsers": [
"5babdba1-1354-4f08-93e2-018cfe113e70",
"f78c005b-690f-4cab-a515-0064bc0d24e2"
],
"managers": ["5babdba1-1354-4f08-93e2-018cfe113e70"],
"realmID": "libvirt",
"ownerType": "User",
"protocolVer": 1,
"resources": {
"vms": 1,
"vcpus": 16,
"memory": 17179869184,
"devices": {"gpu-nvidia": 0}
},
"vpn": {"host": "127.0.0.1", "port": 30336},
"ssh": {"host": "127.0.0.1", "port": 30080},
"lastPing": 1747955324629,
"start": 1747838038956,
"config": "d022a3ca-dd93-4aad-a783-40b6b47bf6d9",
"vmcInfo": []
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/vms/libvirt/active?token=YOUR_TOKEN'

Create VM Image

POST /api/vms/images

Token type: VM   Read-only: Blocked

Creates a new VM image in the specified realm.

Request body:

FieldTypeRequiredDescription
realmstringYesRealm ID
namestringNoName for the image
descriptionstringNoShort description
sizenumberNoImage size in bytes
imageTypestringNo"windows" or "linux"
infoLibvirtExtraInfoYesRealm-specific image info
limitnumberNoMax concurrent VMs from this image
settingsobjectNoFrontend-specific settings (see below)

Settings options:

FieldOptionsDescription
driveFormatsext4, ntfs, btrfs, xfs, zfsAllowed drive formats for VMs running this image

Response:

StatusDescription
200 OKReturns the created ImageInfo object
400 Bad RequestMissing required fields
401 UnauthorizedInvalid or missing token
403 ForbiddenRead-only token

Returns an ImageInfo object on success.

Example request:

{
"realm": "libvirt",
"name": "Rocky 8 Custom",
"description": "Custom Rocky Linux 8 image",
"size": 5368709120,
"imageType": "linux",
"info": {
"pool": "ticrypt-vm-drives",
"volume": "rocky8-prod.qcow2",
"deviceBusType": "virtio"
},
"limit": 5,
"settings": {
"driveFormats": ["ext4", "xfs"]
}
}

cURL example:

curl --request POST \
--url 'https://ticrypt.yourinstitution.edu/api/vms/images?token=YOUR_TOKEN' \
--header 'Content-Type: application/json' \
--data @payload.json

Get Bricks v3.14.1+

GET /api/vms/bricks

Token type: VM

Lists all Hardware Setups (Bricks) on the system.

Before 3.14.1 a Brick could be modified with Edit Brick but never read, so its id had to come from outside the API. This route closes that.

Request body: None

Response:

StatusDescription
200 OKReturns an array of BrickInfo objects
401 UnauthorizedInvalid or missing token

BrickInfo is defined under Edit Brick, including the debug field added in 3.14.1.

Example response:

[
{
"id": "6268d7b2-ece4-449e-af44-cd4313c332f6",
"name": "Slurm",
"description": "Slurm compute node",
"vcores": 4,
"maxMemBytes": 8589934592,
"realmID": "libvirt",
"creatorID": "60eeec74-37db-4f69-bb46-bf4ca4f69e03",
"externalServers": [],
"format": "ext4",
"imageID": "37a0550f-ff3b-4bd1-aaf0-77a461915839",
"created": 1747838038956,
"noQuota": false,
"vmType": "Worker",
"debug": true
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/vms/bricks?token=YOUR_TOKEN'
Related counts

The management interface's hardware-setup table also shows a device count and a count of VM configs per Brick. The VM config count is derived by counting VM Configs whose brick matches the Brick id. The backing field for the device count is not yet settled; it is the one column of that table this route does not fully determine.


Edit Brick

PATCH /api/vms/bricks

Token type: VM   Read-only: Blocked

Updates a Hardware Setup (Brick) used for creating VMs. Only the fields included in params are changed.

Request body:

FieldTypeRequiredDescription
idstringYesID of the brick to edit
paramsBrickParametersYesFields to update

Brick Update Parameters

All fields are optional. Only defined fields are changed.

FieldTypeDescription
namestringShort name for the brick
descriptionstringLonger description, supports markdown
vcoresnumberVirtual CPU cores
maxMemBytesnumberRAM in bytes
setupstringSetup instructions for VM creation
realmIDstringRealm ID
libvirtLibvirtBrickConfigLibvirt-specific config (required if realm driver is libvirt)
whitelistBrickWhitelistTeams and users allowed to access the brick
externalServersstring[]External servers the brick can access
formatstringOS format: "linux" or "windows"

Response:

StatusDescription
200 OKReturns the updated BrickInfo object
400 Bad RequestMissing id or malformed params
401 UnauthorizedInvalid or missing token
403 ForbiddenRead-only token
404 Not FoundBrick ID does not exist
interface BrickInfo {
id: string;
name: string;
description: string;
setup?: string; // Markdown setup instructions
vcores: number;
maxMemBytes: number;
realmID: string;
libvirt?: LibvirtBrickConfig;
whitelist?: BrickWhitelist;
creatorID: string;
externalServers: string[];
format?: string; // "linux" | "windows"
imageID?: string;
created: number; // Epoch time in millis
modified?: number; // Epoch time in millis
noQuota: boolean; // Default: false
params?: string; // Optional VM parameters
settings?: string; // Frontend-only settings
vmType: "Secure" | "Service" | "Data" | "Worker";
debug: boolean; // As of 3.14.1. Debug mode enabled
}

interface LibvirtBrickConfig {
image: { pool: string; volume: string };
video?: "vnc" | "spice"; // Default: none
pty?: boolean; // Default: false
clockOffset?: "utc" | "localtime"; // Default: "localtime" (Windows), "utc" (others)
nic?: string; // Default: "virtio". Also: "rtl8139"
devices?: { [type: string]: number };
extraXML?: string;
headfullGPU?: string;
deviceBusType?: string;
}

interface BrickWhitelist {
teamIDs: string[];
userIDs: string[];
}

Example request:

{
"id": "dd86080c-062a-4aba-889a-dbf64624e917",
"params": {
"name": "Updated Brick",
"vcores": 8,
"maxMemBytes": 17179869184,
"libvirt": {
"image": {
"pool": "ticrypt-vm-drives",
"volume": "rocky8-prod.qcow2"
},
"video": "vnc",
"pty": true,
"clockOffset": "utc",
"nic": "virtio",
"devices": {"gpu-nvidia": 1}
},
"whitelist": {
"teamIDs": ["team|2d48a972-4dd0-44e6-803f-956dbc56503c"],
"userIDs": []
},
"format": "linux"
}
}

cURL example:

curl --request PATCH \
--url 'https://ticrypt.yourinstitution.edu/api/vms/bricks?token=YOUR_TOKEN' \
--header 'Content-Type: application/json' \
--data @payload.json

Get External Servers v3.14.1+

GET /api/external-servers

Token type: VM

Lists the external servers VMs are permitted to reach, including licensing servers.

BrickInfo.externalServers holds IDs from this list, and four permissions under VM Administration govern these objects — view, create, edit, and delete — but before 3.14.1 no route returned them.

Request body: None

Response:

StatusDescription
200 OKReturns an array of ExternalServerInfo objects
401 UnauthorizedInvalid or missing token
interface ExternalServerInfo {
id: string;
server: string; // Hostname or IP
ports: string; // Port or comma-separated port list
protocol: string; // "tcp" | "udp"
owner: string; // ID of the user who created it
created: number; // Epoch time in millis
active: boolean; // Whether VMs may currently reach it
group?: string; // Optional grouping label
deactivation?: number; // Epoch millis, scheduled deactivation
}

Example response:

[
{
"id": "a7b13740-e39d-40a3-95ea-c2da2c9fd29b",
"server": "ticrypt.com",
"ports": "8080",
"protocol": "tcp",
"owner": "ef8cce01-2c19-4581-b569-47cf5c9d471e",
"created": 1615009365805,
"active": true
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/external-servers?token=YOUR_TOKEN'

Team Routes

Shared Structures

TeamInfo

interface TeamInfo {
id: string;
name: string;
desc: string;
created: number; // Epoch time in millis
modified: number; // Epoch time in millis
quotas: { [key: string]: number }; // Legacy quota representation
teamQuotas: QuotasTyped;
perUserQuotas: QuotasTyped;
settings: object;
startDate?: number; // Epoch millis, resource access start
endDate?: number; // Epoch millis, resource access end
members: TeamMemberInfo[];
}

interface TeamMemberInfo {
id: string;
firstName: string; // Empty string if user no longer exists
lastName: string;
email?: string;
lastLogin?: number; // Epoch time in millis
joined: number; // As of 3.14.1. Epoch millis, joined the team
modified: number; // As of 3.14.1. Epoch millis, membership change
perm: number; // As of 3.14.1. Per-team permission level
}
Membership records v3.14.1+

Before 3.14.1 TeamMemberInfo carried identity only, so a membership record could not be reconstructed: there was no join date and no per-team permission. joined, modified, and perm close that.

QuotasTyped

interface QuotasTyped {
cores?: number; // undefined = no limit
memory?: number; // In bytes. undefined = no limit
storage?: { [storageType: string]: number }; // In bytes. Empty map = no limits
devices?: { [deviceType: string]: number }; // Empty map = no limits
}

Route Summary

RouteMethodDescription
/api/teamsGETGet Teams
/api/teamsPOSTCreate Team
/api/teams/quotasGETGet Team Quotas
/api/teams/quotasPATCHEdit Team Quotas
/api/teams/usageGETGet Team Usage v3.14.1+

Get Teams

GET /api/teams

Token type: Team

Returns all teams on the system.

Request body: None

Response:

StatusDescription
200 OKReturns an array of TeamInfo objects
401 UnauthorizedInvalid or missing token

Example response:

[
{
"id": "team|2d48a972-4dd0-44e6-803f-956dbc56503c",
"name": "Genomics Research Lab",
"desc": "Dr. Chen's genomics team",
"created": 1740000000000,
"modified": 1747000000000,
"quotas": {},
"teamQuotas": {
"cores": 64,
"memory": 137438953472,
"storage": {"vault": 1099511627776}
},
"perUserQuotas": {
"cores": 16,
"memory": 34359738368
},
"settings": {},
"members": [
{"id": "60eeec74-37db-4f69-bb46-bf4ca4f69e03", "firstName": "Jane", "lastName": "Chen", "email": "jchen@example.edu", "lastLogin": 1747955324629}
]
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/teams?token=YOUR_TOKEN'

Create Team

POST /api/teams

Token type: Team   Read-only: Blocked

Creates a new team.

Request body:

FieldTypeRequiredDescription
namestringYesName of the team
descstringYesDescription of the team

Response:

StatusDescription
200 OKReturns the created TeamInfo object
400 Bad RequestMissing required fields
401 UnauthorizedInvalid or missing token
403 ForbiddenRead-only token

Example request:

{
"name": "New Research Team",
"desc": "Biostatistics collaboration group"
}

cURL example:

curl --request POST \
--url 'https://ticrypt.yourinstitution.edu/api/teams?token=YOUR_TOKEN' \
--header 'Content-Type: application/json' \
--data '{"name": "New Research Team", "desc": "Biostatistics collaboration group"}'

Get Team Quotas

GET /api/teams/quotas

Token type: Team

Returns the quotas for a specific team.

Request body:

FieldTypeRequiredDescription
teamstringYesTeam ID

Response:

StatusDescription
200 OKReturns a QuotasTyped object
400 Bad RequestMissing team field
401 UnauthorizedInvalid or missing token
404 Not FoundTeam does not exist

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/teams/quotas?team=team%7C2d48a972-4dd0-44e6-803f-956dbc56503c&token=YOUR_TOKEN'

Edit Team Quotas

PATCH /api/teams/quotas

Token type: Team   Read-only: Blocked

Updates the quotas for a specific team.

Request body:

FieldTypeRequiredDescription
teamstringYesTeam ID
quotasQuotasTypedYesNew quota values

Response:

StatusDescription
200 OKReturns the updated TeamInfo object
400 Bad RequestMissing required fields
401 UnauthorizedInvalid or missing token
403 ForbiddenRead-only token
404 Not FoundTeam does not exist

Example request:

{
"team": "team|2d48a972-4dd0-44e6-803f-956dbc56503c",
"quotas": {
"cores": 128,
"memory": 274877906944,
"storage": {
"vault": 1099511627776,
"drives": 2199023255552
},
"devices": {
"gpu-nvidia": 4
}
}
}

cURL example:

curl --request PATCH \
--url 'https://ticrypt.yourinstitution.edu/api/teams/quotas?token=YOUR_TOKEN' \
--header 'Content-Type: application/json' \
--data @payload.json

Get Team Usage v3.14.1+

GET /api/teams/usage

Token type: Team

Reports actual resource consumption per team, as a counterpart to the limits returned by Get Team Quotas.

Drive and VM consumption can be approximated by listing Drives and Active VMs and grouping by team, but that costs a full listing per request and counts only running VMs. Vault file usage has no such path at all.

Request body: None

Response:

StatusDescription
200 OKReturns an array of UsageInfo objects
401 UnauthorizedInvalid or missing token
interface UsageInfo {
teamID: string; // "" for resources belonging to no team
files: {
cnt: number; // Number of Vault files
size: number; // Total bytes
};
vms: {
cnt: number; // Number of VMs
mem: number; // Total memory in bytes
cores: number; // Total vCores
};
drives: {
cnt: number; // Number of drives
size: number; // Total provisioned bytes
used: number; // Total consumed bytes
};
}

Example response:

[
{
"teamID": "team|2d48a972-4dd0-44e6-803f-956dbc56503c",
"files": {"cnt": 412, "size": 8589934592},
"vms": {"cnt": 3, "mem": 38654705664, "cores": 36},
"drives": {"cnt": 12, "size": 1099511627776, "used": 7391627776}
}
]
Untagged resources

Include a row with an empty teamID covering resources that belong to no team. The management interface renders that row as No Team Assigned, and omitting it makes system-wide totals disagree with the per-team figures.

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/teams/usage?token=YOUR_TOKEN'

Project Routes

Shared Structures

ProjectInfo

interface ProjectInfo {
id: string;
name: string;
desc: string; // Supports markdown
securityLevel: string; // ID of required security level
parent?: string; // Parent project ID (for sub-projects)
settings: object; // Frontend-specific JSON settings
created: number; // Epoch time in millis
modified: number; // Epoch time in millis
}

securityLevel is an ID. Resolve it to a name with Get Security Levels.

Route Summary

RouteMethodDescription
/api/projectsGETGet Projects
/api/projects/<projectID>GETGet Project
/api/projects/membershipsGETGet Project Memberships v3.14.1+
/api/projects/usageGETGet Project Usage v3.14.1+

Get Projects

GET /api/projects

Token type: Project

Returns all projects on the system.

Request body: None

Response:

StatusDescription
200 OKReturns an array of ProjectInfo objects
401 UnauthorizedInvalid or missing token

Example response:

[
{
"id": "proj|38fa1dae-748e-45ae-8a1f-c799a4840a18",
"name": "CMMC Enclave Study",
"desc": "DoD-funded research project under CMMC Level 2",
"securityLevel": "cui",
"settings": {},
"created": 1740000000000,
"modified": 1747000000000
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/projects?token=YOUR_TOKEN'

Get Project

GET /api/projects/<projectID>

Token type: Project

Returns information about a specific project.

Request body: None

Response:

StatusDescription
200 OKReturns a ProjectInfo object
401 UnauthorizedInvalid or missing token
404 Not FoundProject does not exist

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/projects/proj%7C38fa1dae-748e-45ae-8a1f-c799a4840a18?token=YOUR_TOKEN'
URL Encoding

Project IDs contain the | character, which must be URL-encoded as %7C when used in the path.


Get Project Memberships v3.14.1+

GET /api/projects/memberships

Token type: Project

Lists every project membership on the system. ProjectInfo carries no member list, so before 3.14.1 project membership was not reachable at all.

Request body: None

Response:

StatusDescription
200 OKReturns an array of ProjectMembershipInfo objects
401 UnauthorizedInvalid or missing token
interface ProjectMembershipInfo {
projectID: string;
userID: string;
joined: number; // Epoch time in millis
modified: number; // Epoch time in millis
manager: boolean; // Whether the user manages the project
restrictions: string; // Comma-separated restrictions, e.g. "Download"
expiration?: number; // Epoch millis, membership expiry
}

Example response:

[
{
"projectID": "proj|5a822d18-1923-442f-8dc9-ede14209c042",
"userID": "f3e8aec7-f23f-4425-bdcc-7c011e2b3377",
"joined": 1761341562745,
"modified": 1761341562745,
"manager": true,
"restrictions": "Download"
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/projects/memberships?token=YOUR_TOKEN'
Inherited membership

State whether the response includes memberships a user holds through a parent project, or only memberships recorded directly against each project. Member counts in the management interface and the sum of these records do not agree on every project, and the difference tracks the project hierarchy.


Get Project Usage v3.14.1+

GET /api/projects/usage

Token type: Project

Reports actual resource consumption per project. Same shape as Get Team Usage, keyed by project.

Request body: None

Response:

StatusDescription
200 OKReturns an array of ProjectUsageInfo objects
401 UnauthorizedInvalid or missing token
interface ProjectUsageInfo {
projectID: string; // "" for resources belonging to no project
files: { cnt: number; size: number };
vms: { cnt: number; mem: number; cores: number };
drives: { cnt: number; size: number; used: number };
}

Example response:

[
{
"projectID": "proj|5a822d18-1923-442f-8dc9-ede14209c042",
"files": {"cnt": 88, "size": 2147483648},
"vms": {"cnt": 1, "mem": 8589934592, "cores": 4},
"drives": {"cnt": 2, "size": 214748364800, "used": 1443109171}
}
]
Untagged resources

As with team usage, include a row with an empty projectID. The management interface renders it as No project.

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/projects/usage?token=YOUR_TOKEN'

Security Routes

Security levels gate access to projects, and security requirements are the conditions that make up a level. Both are reached with a Project token. Per-user certifications against these requirements are a User route: see Get User Certifications.

Route Summary

RouteMethodDescription
/api/security-requirementsGETGet Security Requirements v3.14.1+
/api/security-levelsGETGet Security Levels v3.14.1+

Get Security Requirements v3.14.1+

GET /api/security-requirements

Token type: Project

Lists the security requirements defined on the system.

Request body: None

Response:

StatusDescription
200 OKReturns an array of SecurityRequirementInfo objects
401 UnauthorizedInvalid or missing token
interface SecurityRequirementInfo {
id: string; // "sreq|<uuid>"
name: string;
type: string; // Requirement type, e.g. "UserCert"
created: number; // Epoch time in millis
modified: number; // Epoch time in millis
levelIDs: string[]; // Security levels that include this requirement
certificationCount: number;// Users certified against it
}

Example response:

[
{
"id": "sreq|01064193-e291-4b6e-a41e-d8eccf2debe9",
"name": "Test Requirement",
"type": "UserCert",
"created": 1531191741234,
"modified": 1643922519071,
"levelIDs": ["slvl|00e09525-61ee-4259-bd69-b2ed745e685d"],
"certificationCount": 3
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/security-requirements?token=YOUR_TOKEN'

Get Security Levels v3.14.1+

GET /api/security-levels

Token type: Project

Lists the security levels defined on the system. ProjectInfo.securityLevel holds an ID from this list; before 3.14.1 nothing resolved it to a name.

Request body: None

Response:

StatusDescription
200 OKReturns an array of SecurityLevelInfo objects
401 UnauthorizedInvalid or missing token
interface SecurityLevelInfo {
id: string; // "slvl|<uuid>"
name: string;
created: number; // Epoch time in millis
modified: number; // Epoch time in millis
requirementIDs: string[]; // Requirements that make up this level
projectCount: number; // Projects currently at this level
}

Example response:

[
{
"id": "slvl|00e09525-61ee-4259-bd69-b2ed745e685d",
"name": "Empty",
"created": 1509400773572,
"modified": 1681694681035,
"requirementIDs": [],
"projectCount": 60
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/security-levels?token=YOUR_TOKEN'

User Routes

Shared Structures

UserInfo

interface UserInfo {
id: string;
firstName: string;
lastName: string;
email: string; // Login email / username
contactEmail?: string; // Contact email if different from login
joined: number; // Epoch time in millis
lastLogin?: number; // Epoch time in millis
role: string;
permissions: string[];
state: string; // Account state
stateReason?: string;
deactivationType?: DeactivationType;
department?: string;
position?: string;
comment?: string; // Admin-visible notes
neverEscrow?: boolean;
passChanged?: number; // Epoch millis, last SSA credential change
expiration?: number; // Epoch millis, account expiration
tosAccepted?: number; // Epoch millis, last ToS acceptance
deactivatedUntil?: number; // Epoch millis, temporary deactivation end
lastEscrow?: number; // Epoch millis, last escrow date
}

permissions holds raw permission codes. The API does not resolve them to names or descriptions: that mapping lives in the management interface, not the backend, so a client displaying permissions supplies its own.

DeactivationType

ValueDescription
AdminDisabled by an administrator
FailedPasswordDisabled due to consecutive failed password attempts
TimeoutDisabled due to account inactivity
XSSDisabled due to detected XSS attack attempt
RegistrationDisabled pending account activation

Route Summary

RouteMethodDescription
/api/usersGETList Users
/api/users/disablePOSTDeactivate Users
/api/users/certificationsGETGet User Certifications v3.14.1+
/api/users/managed-objectsGETGet Managed Objects v3.14.1+

List Users

GET /api/users

Token type: User

Lists all users on the system.

Request body: None

Response:

StatusDescription
200 OKReturns an array of UserInfo objects
401 UnauthorizedInvalid or missing token

Example response:

[
{
"id": "60eeec74-37db-4f69-bb46-bf4ca4f69e03",
"firstName": "Jane",
"lastName": "Chen",
"email": "jchen@example.edu",
"joined": 1720000000000,
"lastLogin": 1747955324629,
"role": "User",
"permissions": ["drive.create", "vm.launch"],
"state": "Active"
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/users?token=YOUR_TOKEN'

Deactivate Users

POST /api/users/disable

Token type: User   Read-only: Blocked

Disables one or more users by ID. Super-admins cannot be disabled.

Request body:

FieldTypeRequiredDescription
usersstring[]YesArray of user IDs to deactivate

Response:

StatusDescription
200 OKUsers deactivated successfully
400 Bad RequestMissing or empty users array
401 UnauthorizedInvalid or missing token
403 ForbiddenRead-only token, or attempted to deactivate a super-admin

Example request:

{
"users": ["a3770b72-7d09-4493-910b-c7dec7417d78"]
}

cURL example:

curl --request POST \
--url 'https://ticrypt.yourinstitution.edu/api/users/disable?token=YOUR_TOKEN' \
--header 'Content-Type: application/json' \
--data '{"users": ["a3770b72-7d09-4493-910b-c7dec7417d78"]}'

Get User Certifications v3.14.1+

GET /api/users/certifications

Token type: User

Lists every certification recorded against a security requirement, one record per user and requirement.

Request body: None

Response:

StatusDescription
200 OKReturns an array of UserCertificationInfo objects
401 UnauthorizedInvalid or missing token
interface UserCertificationInfo {
requirementID: string; // "sreq|<uuid>"
userID: string;
expires?: number; // Epoch millis. Absent means no expiry
expired: boolean; // Whether it has lapsed
created: number; // Epoch time in millis
modified: number; // Epoch time in millis
modifiedBy: string; // ID of the user who last changed it
}

Example response:

[
{
"requirementID": "sreq|b4819bd8-6aeb-4c4f-a64f-c559fee99df3",
"userID": "ee39f64e-d7fc-4f93-9675-a741817b7153",
"expires": 1780601233660,
"expired": true,
"created": 1749064403466,
"modified": 1749065234935,
"modifiedBy": "ee39f64e-d7fc-4f93-9675-a741817b7153"
}
]

Resolve requirementID with Get Security Requirements.

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/users/certifications?token=YOUR_TOKEN'

Get Managed Objects v3.14.1+

GET /api/users/managed-objects

Token type: User

Lists the teams and projects each sub-admin has been assigned to manage.

Request body: None

Response:

StatusDescription
200 OKReturns an array of ManagedObjectInfo objects
401 UnauthorizedInvalid or missing token
interface ManagedObjectInfo {
managerID: string; // ID of the managing user
objectType: "Team" | "Project";
objectID: string; // Team or project ID
}

Example response:

[
{
"managerID": "f7034bb7-5bb6-47a4-8239-c8cfe4e8bb44",
"objectType": "Team",
"objectID": "team|23dcef51-51f8-4a3e-aebc-9674bc983543"
}
]

cURL example:

curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/users/managed-objects?token=YOUR_TOKEN'

System Snapshot

The management interface can export object lists as JSON one table at a time. The snapshot script calls the routes above and writes the same files without a browser, so a system inventory can be captured on a schedule and kept for comparison.

Two downloads, for different jobs:

ScriptRequiresProducesUse when
ticrypt-snapshot.pyPython 3.9 or newer, standard library onlyAll 15 exports, with derived columns, plus the gap manifestDefault. Reproduces what the management interface exports
ticrypt-snapshot.shcurl. jq optional, for formatting onlyRaw API responses, one file per routePython is unavailable, or you want the unmodified JSON to transform yourself

The bash script is a collector, not a second implementation. It does not build the 15 exports: the derived columns those need — quota percentages, per-team and per-project rollups, rank derivation — live only in the Python script.

Which one to reach for

On RHEL, Rocky, and AlmaLinux 8 through 10, Python 3 and curl are present out of the box and jq is not. The Python script is therefore the one with nothing to install on a stock backend host. Reach for the bash collector when Python is unavailable or policy forbids it.

The two compose. Collect on a restricted host, then normalize anywhere: see Raw collection with bash.

Step 1 — Create the API key

The snapshot reads users, teams, drives, VMs, and projects, so its key needs every token type. On 3.14.1 that is one key.

  1. Open Management → Miscellaneous → API Keys. The full click-path is in API Keys.
  2. Click Create new API key.
  3. Select every key typeDrive, VM, Team, Project, and User. See Multi-Type Tokens.
  4. Mark the key read-only. The snapshot never writes. A read-only key that leaks cannot be turned against the system.
  5. Set an expiration you are willing to rotate on, and give the key a description naming what runs it.
  6. Click Create, then Copy.
Copy the key now

The key value is shown once, at creation, and is never stored by the server. If you lose it, delete the key and create a new one.

On deployments below 3.14.1 a key holds a single type, so create five keys — one per type — and pass them separately in step 2.

Step 2 — Give the script the key

Pass the key through the environment rather than as an argument. Command-line arguments land in shell history and are visible to any user who can run ps:

export TICRYPT_BASE_URL=https://ticrypt.yourinstitution.edu
export TICRYPT_TOKEN=YOUR_KEY

The script reads TICRYPT_TOKEN in preference to --token, so the environment wins where both are set.

For the pre-3.14.1 path, set one variable per type instead. Any of TICRYPT_TOKEN_USER, TICRYPT_TOKEN_TEAM, TICRYPT_TOKEN_DRIVE, TICRYPT_TOKEN_VM, TICRYPT_TOKEN_PROJECT:

export TICRYPT_TOKEN_USER=USER_KEY
export TICRYPT_TOKEN_TEAM=TEAM_KEY
export TICRYPT_TOKEN_DRIVE=DRIVE_KEY
export TICRYPT_TOKEN_VM=VM_KEY
export TICRYPT_TOKEN_PROJECT=PROJECT_KEY

Step 3 — Run it

python3 ticrypt-snapshot.py

Or, for raw collection:

./ticrypt-snapshot.sh

That is the whole command once the environment is set. Either script prints each file as it writes it, then a summary of anything it could not capture.

If a key is rejected, the script reports which scope failed and stops. A 401 or 403 on one scope and not others means the key is missing that type: check the key in Management → Miscellaneous → API Keys.

TLS verification is always on and cannot be turned off, so the backend must present a certificate the host running the script already trusts. A failure here reports as a connection error naming the certificate, not as an authentication error.

Equivalent forms, where passing the key on the command line is acceptable:

# One key with all types selected
python3 ticrypt-snapshot.py \
--base-url https://ticrypt.yourinstitution.edu \
--token YOUR_KEY

# Or one key per type, for deployments below 3.14.1
python3 ticrypt-snapshot.py \
--base-url https://ticrypt.yourinstitution.edu \
--token-user USER_KEY --token-team TEAM_KEY --token-drive DRIVE_KEY \
--token-vm VM_KEY --token-project PROJECT_KEY

Step 4 — Read the manifest

Every run writes _snapshot-manifest.json alongside the exports. It records the base URL, the time, which auth mode was used, and every field the run could not capture with the reason for each. Read it before treating a snapshot as complete.

Options

OptionEffect
--realmsComma-separated realm IDs. Defaults to libvirt. Unnecessary on 3.14.1, where the script reads Get Realms
--out-dirOutput directory. Defaults to ./ticrypt-snapshot-<date>
--rawAlso write raw/, the unmodified API responses
--timeoutPer-request timeout in seconds. Defaults to 30

Output

Files are named to match the management interface, so a snapshot drops in alongside exports taken by hand. Both the record count and the date are computed at run time:

ticrypt-snapshot-<YYYY-MM-DD>/ # directory: the date of the run
├── <count>-<export> (<M>-<D>-<YYYY>).json # one per export type
└── _snapshot-manifest.json

<count> is the number of records written for that export on that run, and the date is the day the script ran. Two runs a week apart produce different filenames, and a system that grew between them produces different counts. Nothing in the name is fixed.

The two date formats differ on purpose:

PositionFormatWhy
DirectoryISO, zero-paddedRuns sort chronologically
FilenameM-D-YYYY, not zero-paddedMatches the management interface exactly

A run against one deployment, on 16 September 2026, produced the following. Your counts and dates will differ — this shows the shape, not the values:

ticrypt-snapshot-2026-09-16/
├── 441-users (9-16-2026).json
├── 156-teams (9-16-2026).json
├── 1173-team_memberships (9-16-2026).json
├── 572-drives (9-16-2026).json
├── 339-projects (9-16-2026).json
├── 160-project_memberships (9-16-2026).json
├── 340-resources_by_project (9-16-2026).json
├── 79-security_requirements (9-16-2026).json
├── 41-security_levels (9-16-2026).json
├── 11-user_profiles (9-16-2026).json
├── 194-user_certifications (9-16-2026).json
├── 25-sub-admin_managed_objects (9-16-2026).json
├── 8-vm_images (9-16-2026).json
├── 7-vm_hardware_setups (9-16-2026).json
├── 36-licensing_servers (9-16-2026).json
└── _snapshot-manifest.json

An export with no route behind it lands as 0-. All fifteen files are written on every run whether or not a route backs them, so a snapshot has the same shape each time and two runs stay comparable.

Raw collection with bash

ticrypt-snapshot.sh fetches every documented read route and writes the response untouched, one file per route. It needs only curl; jq is used to format the output when present, and the script runs without it.

It takes the same options and environment variables as the Python script, including reading tokens from the environment in preference to the command line. TLS verification is likewise always on.

export TICRYPT_BASE_URL=https://ticrypt.yourinstitution.edu
export TICRYPT_TOKEN=YOUR_KEY
./ticrypt-snapshot.sh

Output lands in ./ticrypt-snapshot-raw-<YYYY-MM-DD>/, one file per route, plus _collection-manifest.json recording every route's HTTP status and the reason for any that were skipped:

ticrypt-snapshot-raw-<YYYY-MM-DD>/
├── api-users.json
├── api-teams.json
├── api-drives.json
├── api-projects.json
├── api-vms-libvirt-active.json
├── api-vms-libvirt-configs.json
├── api-vms-images-libvirt.json
└── _collection-manifest.json

The ten routes added in 3.14.1 return 404 until the backend serves them. The collector records each as skipped and carries on, so the same script works before and after the upgrade.

Feeding the collector's output to the Python script

The filenames match what --replay expects, so a directory collected by the bash script can be normalized into the full 15 exports on any machine with Python:

./ticrypt-snapshot.sh --out-dir snapshot-raw # on the restricted host
python3 ticrypt-snapshot.py --replay snapshot-raw # anywhere else

The result is identical to running the Python script directly against the backend. This is the reason to use the collector: the host holding the API key needs only curl, and no credential ever reaches the machine doing the transformation.