tiCrypt REST API
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.
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.1in 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 Type | Available Routes |
|---|---|
Drive | Drive Routes |
VM | VM Routes |
Team | Team Routes |
Project | Project Routes |
User | User 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:
| Method | Example |
|---|---|
| URL parameter | /api/drives?token=YOUR_TOKEN |
| Cookie header | Cookie: TC_TOKEN=YOUR_TOKEN |
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)
}
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.
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:
| Route | Method | Description |
|---|---|---|
/api/drives | GET | Get Drives |
/api/realms | GET | Get Realms v3.14.1+ |
/api/vms/images | GET | Get VM Images |
/api/vms/bricks | GET | Get Bricks v3.14.1+ |
/api/vms/<realmID>/active | GET | Get Active VMs for Realm |
/api/vms/<realmID>/configs | GET | Get VM Configs |
/api/external-servers | GET | Get External Servers v3.14.1+ |
/api/teams | GET | Get Teams |
/api/teams/quotas | GET | Get Team Quotas |
/api/teams/usage | GET | Get Team Usage v3.14.1+ |
/api/projects | GET | Get Projects |
/api/projects/<projectID> | GET | Get Project |
/api/projects/memberships | GET | Get Project Memberships v3.14.1+ |
/api/projects/usage | GET | Get Project Usage v3.14.1+ |
/api/security-requirements | GET | Get Security Requirements v3.14.1+ |
/api/security-levels | GET | Get Security Levels v3.14.1+ |
/api/users | GET | List Users |
/api/users/certifications | GET | Get User Certifications v3.14.1+ |
/api/users/managed-objects | GET | Get Managed Objects v3.14.1+ |
Routes blocked for read-only tokens:
| Route | Method | Description |
|---|---|---|
/api/vms/images | POST | Create VM Image |
/api/vms/bricks | PATCH | Edit Brick |
/api/teams | POST | Create Team |
/api/teams/quotas | PATCH | Edit Team Quotas |
/api/users/disable | POST | Deactivate 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
| Status | Meaning |
|---|---|
200 OK | Request succeeded |
400 Bad Request | Malformed request body or missing required fields |
401 Unauthorized | Missing or invalid token |
403 Forbidden | Token does not have permission for this operation (e.g., read-only token on a mutating route, or wrong token type) |
404 Not Found | The requested resource does not exist |
422 Unprocessable Entity | Request body is valid JSON but contains invalid values |
500 Internal Server Error | Unexpected server error |
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.
| Route | Method | Token type | Section |
|---|---|---|---|
/api/realms | GET | VM | Get Realms |
/api/vms/bricks | GET | VM | Get Bricks |
/api/external-servers | GET | VM | Get External Servers |
/api/teams/usage | GET | Team | Get Team Usage |
/api/projects/memberships | GET | Project | Get Project Memberships |
/api/projects/usage | GET | Project | Get Project Usage |
/api/security-requirements | GET | Project | Get Security Requirements |
/api/security-levels | GET | Project | Get Security Levels |
/api/users/certifications | GET | User | Get User Certifications |
/api/users/managed-objects | GET | User | Get 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
| Route | Method | Description |
|---|---|---|
/api/drives | GET | Get Drives |
Get Drives
GET /api/drives
Token type: Drive
Lists all drives on the system.
Request body: None
Response:
| Status | Description |
|---|---|
200 OK | Returns an array of DriveInfo objects |
401 Unauthorized | Invalid 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
}
The noBackup field is deprecated and defaults to true. It will be removed in a future version.
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
| Route | Method | Description |
|---|---|---|
/api/realms | GET | Get Realms v3.14.1+ |
/api/vms/images | GET | Get VM Images |
/api/vms/images | POST | Create VM Image |
/api/vms/bricks | GET | Get Bricks (Hardware Setups) v3.14.1+ |
/api/vms/bricks | PATCH | Edit Brick (Hardware Setup) |
/api/vms/<realmID>/active | GET | Get Active VMs for Realm |
/api/vms/<realmID>/configs | GET | Get VM Configs |
/api/external-servers | GET | Get 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:
| Status | Description |
|---|---|
200 OK | Returns an array of RealmInfo objects |
401 Unauthorized | Invalid 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:
| Field | Type | Required | Description |
|---|---|---|---|
realm | string | Yes | Realm ID to list images for |
Response:
| Status | Description |
|---|---|
200 OK | Returns an array of ImageInfo objects |
400 Bad Request | Missing realm field |
401 Unauthorized | Invalid 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:
| Status | Description |
|---|---|
200 OK | Returns an array of VMConfigInfo objects |
401 Unauthorized | Invalid 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
}
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:
| Status | Description |
|---|---|
200 OK | Returns an array of VirtualMachine objects |
401 Unauthorized | Invalid 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:
| Field | Type | Required | Description |
|---|---|---|---|
realm | string | Yes | Realm ID |
name | string | No | Name for the image |
description | string | No | Short description |
size | number | No | Image size in bytes |
imageType | string | No | "windows" or "linux" |
info | LibvirtExtraInfo | Yes | Realm-specific image info |
limit | number | No | Max concurrent VMs from this image |
settings | object | No | Frontend-specific settings (see below) |
Settings options:
| Field | Options | Description |
|---|---|---|
driveFormats | ext4, ntfs, btrfs, xfs, zfs | Allowed drive formats for VMs running this image |
Response:
| Status | Description |
|---|---|
200 OK | Returns the created ImageInfo object |
400 Bad Request | Missing required fields |
401 Unauthorized | Invalid or missing token |
403 Forbidden | Read-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:
| Status | Description |
|---|---|
200 OK | Returns an array of BrickInfo objects |
401 Unauthorized | Invalid 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'
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:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | ID of the brick to edit |
params | BrickParameters | Yes | Fields to update |
Brick Update Parameters
All fields are optional. Only defined fields are changed.
| Field | Type | Description |
|---|---|---|
name | string | Short name for the brick |
description | string | Longer description, supports markdown |
vcores | number | Virtual CPU cores |
maxMemBytes | number | RAM in bytes |
setup | string | Setup instructions for VM creation |
realmID | string | Realm ID |
libvirt | LibvirtBrickConfig | Libvirt-specific config (required if realm driver is libvirt) |
whitelist | BrickWhitelist | Teams and users allowed to access the brick |
externalServers | string[] | External servers the brick can access |
format | string | OS format: "linux" or "windows" |
Response:
| Status | Description |
|---|---|
200 OK | Returns the updated BrickInfo object |
400 Bad Request | Missing id or malformed params |
401 Unauthorized | Invalid or missing token |
403 Forbidden | Read-only token |
404 Not Found | Brick 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:
| Status | Description |
|---|---|
200 OK | Returns an array of ExternalServerInfo objects |
401 Unauthorized | Invalid 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
}
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
| Route | Method | Description |
|---|---|---|
/api/teams | GET | Get Teams |
/api/teams | POST | Create Team |
/api/teams/quotas | GET | Get Team Quotas |
/api/teams/quotas | PATCH | Edit Team Quotas |
/api/teams/usage | GET | Get Team Usage v3.14.1+ |
Get Teams
GET /api/teams
Token type: Team
Returns all teams on the system.
Request body: None
Response:
| Status | Description |
|---|---|
200 OK | Returns an array of TeamInfo objects |
401 Unauthorized | Invalid 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:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Name of the team |
desc | string | Yes | Description of the team |
Response:
| Status | Description |
|---|---|
200 OK | Returns the created TeamInfo object |
400 Bad Request | Missing required fields |
401 Unauthorized | Invalid or missing token |
403 Forbidden | Read-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:
| Field | Type | Required | Description |
|---|---|---|---|
team | string | Yes | Team ID |
Response:
| Status | Description |
|---|---|
200 OK | Returns a QuotasTyped object |
400 Bad Request | Missing team field |
401 Unauthorized | Invalid or missing token |
404 Not Found | Team 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:
| Field | Type | Required | Description |
|---|---|---|---|
team | string | Yes | Team ID |
quotas | QuotasTyped | Yes | New quota values |
Response:
| Status | Description |
|---|---|
200 OK | Returns the updated TeamInfo object |
400 Bad Request | Missing required fields |
401 Unauthorized | Invalid or missing token |
403 Forbidden | Read-only token |
404 Not Found | Team 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:
| Status | Description |
|---|---|
200 OK | Returns an array of UsageInfo objects |
401 Unauthorized | Invalid 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}
}
]
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
| Route | Method | Description |
|---|---|---|
/api/projects | GET | Get Projects |
/api/projects/<projectID> | GET | Get Project |
/api/projects/memberships | GET | Get Project Memberships v3.14.1+ |
/api/projects/usage | GET | Get Project Usage v3.14.1+ |
Get Projects
GET /api/projects
Token type: Project
Returns all projects on the system.
Request body: None
Response:
| Status | Description |
|---|---|
200 OK | Returns an array of ProjectInfo objects |
401 Unauthorized | Invalid 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:
| Status | Description |
|---|---|
200 OK | Returns a ProjectInfo object |
401 Unauthorized | Invalid or missing token |
404 Not Found | Project does not exist |
cURL example:
curl --request GET \
--url 'https://ticrypt.yourinstitution.edu/api/projects/proj%7C38fa1dae-748e-45ae-8a1f-c799a4840a18?token=YOUR_TOKEN'
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:
| Status | Description |
|---|---|
200 OK | Returns an array of ProjectMembershipInfo objects |
401 Unauthorized | Invalid 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'
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:
| Status | Description |
|---|---|
200 OK | Returns an array of ProjectUsageInfo objects |
401 Unauthorized | Invalid 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}
}
]
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
| Route | Method | Description |
|---|---|---|
/api/security-requirements | GET | Get Security Requirements v3.14.1+ |
/api/security-levels | GET | Get 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:
| Status | Description |
|---|---|
200 OK | Returns an array of SecurityRequirementInfo objects |
401 Unauthorized | Invalid 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:
| Status | Description |
|---|---|
200 OK | Returns an array of SecurityLevelInfo objects |
401 Unauthorized | Invalid 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
| Value | Description |
|---|---|
Admin | Disabled by an administrator |
FailedPassword | Disabled due to consecutive failed password attempts |
Timeout | Disabled due to account inactivity |
XSS | Disabled due to detected XSS attack attempt |
Registration | Disabled pending account activation |
Route Summary
| Route | Method | Description |
|---|---|---|
/api/users | GET | List Users |
/api/users/disable | POST | Deactivate Users |
/api/users/certifications | GET | Get User Certifications v3.14.1+ |
/api/users/managed-objects | GET | Get Managed Objects v3.14.1+ |
List Users
GET /api/users
Token type: User
Lists all users on the system.
Request body: None
Response:
| Status | Description |
|---|---|
200 OK | Returns an array of UserInfo objects |
401 Unauthorized | Invalid 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:
| Field | Type | Required | Description |
|---|---|---|---|
users | string[] | Yes | Array of user IDs to deactivate |
Response:
| Status | Description |
|---|---|
200 OK | Users deactivated successfully |
400 Bad Request | Missing or empty users array |
401 Unauthorized | Invalid or missing token |
403 Forbidden | Read-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:
| Status | Description |
|---|---|
200 OK | Returns an array of UserCertificationInfo objects |
401 Unauthorized | Invalid 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:
| Status | Description |
|---|---|
200 OK | Returns an array of ManagedObjectInfo objects |
401 Unauthorized | Invalid 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:
| Script | Requires | Produces | Use when |
|---|---|---|---|
ticrypt-snapshot.py | Python 3.9 or newer, standard library only | All 15 exports, with derived columns, plus the gap manifest | Default. Reproduces what the management interface exports |
ticrypt-snapshot.sh | curl. jq optional, for formatting only | Raw API responses, one file per route | Python 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.
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.
- Open Management → Miscellaneous → API Keys. The full click-path is in API Keys.
- Click Create new API key.
- Select every key type —
Drive,VM,Team,Project, andUser. See Multi-Type Tokens. - Mark the key read-only. The snapshot never writes. A read-only key that leaks cannot be turned against the system.
- Set an expiration you are willing to rotate on, and give the key a description naming what runs it.
- Click Create, then Copy.
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
| Option | Effect |
|---|---|
--realms | Comma-separated realm IDs. Defaults to libvirt. Unnecessary on 3.14.1, where the script reads Get Realms |
--out-dir | Output directory. Defaults to ./ticrypt-snapshot-<date> |
--raw | Also write raw/, the unmodified API responses |
--timeout | Per-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:
| Position | Format | Why |
|---|---|---|
| Directory | ISO, zero-padded | Runs sort chronologically |
| Filename | M-D-YYYY, not zero-padded | Matches 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.