ASI Cloud Portal API — LLM Skill
You are an assistant that helps users query the ASI Cloud Portal API. Use the reference below to construct correct API requests, interpret responses, and answer questions about the user's cloud infrastructure, billing, and pricing.
Read this first
- Current VMs are
GET /vm-data/costs?current_only=true, notGET /vm-data. Withoutcurrent_only, costs also lists VMs deleted this month (current: false), because they were still billed; the summary always covers every VM billed in the month andsummary.current_vmscounts the ones that exist now./vm-datais a list of hourly snapshots. It is cursor-paged, ordered by name, andpagegreater than 1 returns 422. One VM appears many times. GET /billings/summaryis the latest daily batch only. It is not the largest batch and not the invoice total. A month contains many batches. Sumfull_month_amountperbatch_idand use the largest batch when you need a month figure.organisation_idon VM, billing, and price rows is the numeric id. The customer code (S001,C729) isorganisation.organisation_id. On/vm-data/costs,summary.organisation_idis the code.- Writes need an organisation token. Admin tokens can read. VM deploy, power, rename, resize, delete, and snapshots, every S3 bucket change, and SSH keys, firewall rules and AI Inference (read or write) return 403 for an admin token.
- Turning on AI Inference starts billing, and its key secret is returned once. Confirm before
POST /ai-inference/enable; give thesecretfromPOST /ai-inference/keysto the user straight away. - A generated SSH private key is returned once.
POST /ssh-keyswithoutpublic_keyreturnsprivate_key. It cannot be fetched again. - An S3 secret key is returned once. Create and rotate-key return
credentials.secret_access_key. Give it to the user straight away. It cannot be fetched again; a lost secret needs a key rotation. - A parent organisation token reads that org and its direct child orgs. Writes stay on the token owner's organisation.
Authentication
Authorization: Bearer {{API_TOKEN}}
Create the token on the API Tokens page in the self-service portal. Replace {{API_TOKEN}} with that token. Do not send a Cloudflare Access header.
| Token | Reads | Writes (VM deploy, power, rename, resize, delete, snapshots; S3 bucket changes) |
|---|---|---|
| Organisation | That org, plus direct child orgs | Token owner's org only |
| Admin | Any organisation. organisation_id is required on /vm-data/costs |
403 |
Pass organisation_id (the numeric id) to limit a list to one accessible org. An id outside the token's set returns 404 Organisation not found.
The API host
cloud.asi.nzaccepts the bearer token alone.portal.asi.nzis the staff portal, not this API.
MCP server
If your client supports MCP, connect to https://cloud.asi.nz/mcp with the same Authorization: Bearer {{API_TOKEN}} header instead of calling the REST endpoints yourself. It offers read tools (VMs and costs, VM state and snapshots, regions, templates, SSH keys, firewall rules, S3 buckets, AI Inference status and keys, billing, prices) and write tools for the same products (deploy, power, rename, resize and delete VMs, snapshots, upload SSH keys, firewall rules, S3 buckets, AI Inference keys and on/off), with the same rules and responses as the endpoints below. Destructive tools are annotated so clients ask first. SSH key generation is not offered over MCP (the private key would land in the conversation). Admin tokens are refused.
Base URL
https://cloud.asi.nz/api/v1
Interactive reference: https://cloud.asi.nz/docs/api. OpenAPI: https://cloud.asi.nz/docs/api.json.
Product pricing is a different, public API. See the product skill. It is not part of this document.
Rate Limit
60 requests per minute. A 429 body is {"message": "Too Many Attempts."} and the response includes X-RateLimit-Limit and X-RateLimit-Remaining. per_page above 100 is clamped to 100. It does not return an error.
Deploy has a separate limit: one deployment per user per 60 seconds, body {"error":"Too Many Requests","message":"A VM deployment was recently started. Please wait before deploying another."}.
Pagination
Two shapes. Do not send page to /vm-data.
Offset pages (every list except GET /vm-data)
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
page |
integer | 1 | 1+ | Page number |
per_page |
integer | 15 | 1–100 | Items per page |
{
"data": [],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 15,
"to": 15,
"total": 72
},
"links": {
"first": "https://cloud.asi.nz/api/v1/billings?page=1",
"last": "https://cloud.asi.nz/api/v1/billings?page=5",
"prev": null,
"next": "https://cloud.asi.nz/api/v1/billings?page=2"
}
}
Follow links.next until it is null. Read meta.total from page 1 before walking a large list.
Cursor (GET /vm-data only)
per_page defaults to 15 and clamps to 1–100. There is no total or last_page.
{
"data": [],
"meta": {
"per_page": 15,
"next_cursor": "eyJ...",
"prev_cursor": null
},
"links": {
"next": "https://cloud.asi.nz/api/v1/vm-data?cursor=eyJ...&per_page=15",
"prev": null
}
}
Follow links.next, or pass cursor from meta.next_cursor. page greater than 1 returns 422: Offset pagination via the page parameter is not supported. Follow links.next and pass the cursor parameter instead. page=1 is accepted and marked deprecated. Prefer omitting page.
Endpoints
1. Organisations
Returns the organisations this token can read: the token owner's org, and each direct child org. An admin token lists organisations and may filter with organisation_id.
List organisations
GET /organisations
Optional filters: name (partial match). Admin tokens may also pass organisation_id (numeric id).
Get organisation
GET /organisations/{id}
Response fields:
| Field | Type | Description |
|---|---|---|
id |
integer | Numeric id. This is the organisation_id filter on other endpoints |
name |
string | Organisation name |
organisation_id |
string | Customer code (for example S001 or C729) |
billing_type_id |
integer | Billing method ID |
invoice_reference |
string | Invoice reference template (nullable) |
gst_rate |
string | GST rate (e.g. "15.00") |
invoice_currency |
string | Currency code (e.g. "NZD") |
create_invoice |
integer | Whether invoices are generated (1/0) |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
billing_type |
object | { id, name } — e.g. "Average Usage" |
2. VM Data (Virtual Machines)
GET /vm-data returns hourly sync snapshots, not one row per VM. The same extId appears once per snapshot. Order is name, then newest created_at, then id. Use GET /vm-data/costs for the current VM list and cost. Use GET /vm-data/{id}/state for live power and IP addresses.
List VM snapshots
GET /vm-data
Cursor pagination only. See Pagination.
Filters:
| Parameter | Type | Description |
|---|---|---|
guest_os_id |
integer | Filter by guest OS ID |
power_state |
boolean | true = powered on, false = powered off |
backup |
boolean | true = has backup, false = no backup |
protection_type |
boolean | true = protected, false = unprotected |
created_at_from |
string | ISO 8601 date — records created on or after |
created_at_to |
string | ISO 8601 date — records created on or before |
vcpus_min |
integer | Minimum vCPU count |
vcpus_max |
integer | Maximum vCPU count |
memory_size_gib_min |
integer | Minimum memory in GiB |
memory_size_gib_max |
integer | Maximum memory in GiB |
Boolean filters accept: true, false, 1, 0.
Get VM
GET /vm-data/{id}
Response fields:
| Field | Type | Description |
|---|---|---|
id |
integer | Internal ID |
source_id |
integer | Source (cloud provider) ID |
source_data_type_id |
integer | Data type (1=VM, 3=Backup, 4=Storage, 5=S3) |
batch_id |
string | Processing batch identifier |
extId |
string | External ID from the cloud provider |
name |
string | VM name |
organisation_id |
integer | Numeric organisation id |
total_storage_tib |
number | Total storage in TiB |
used_storage_tib |
number | Used storage in TiB |
used_archive_tib |
number | Used archive storage in TiB |
vcpus |
integer | Number of vCPUs |
memory_size_gib |
number | Memory in GiB |
protection_type |
boolean | Whether VM has protection policy |
power_state |
boolean | true = on, false = off |
classification_id |
integer | Classification ID (nullable) |
backup |
boolean | Whether VM has backup |
guest_os_id |
integer | Guest OS ID (nullable) |
guest_os_license_id |
integer | Guest OS license ID (nullable) |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
source |
object | { id, name } — cloud provider source |
source_data_type |
object | { id, name } — data type |
organisation |
object | { id, name, organisation_id } |
classification |
object | { id, description } (nullable). The name is on GET /classifications/{id} |
guest_os |
object | { id, name, family } (nullable). Architecture is on GET /guest-os/{id} |
guest_os_license |
object | { id, name } (nullable) |
Get VM change history
GET /vm-data/{id}/history
Returns the change history for a specific VM, showing how its configuration has changed over time. Records are matched by extId and limited to VM-type source data. Paginated, sorted by most recent first.
Response fields (per history entry):
| Field | Type | Description |
|---|---|---|
id |
integer | Internal ID |
name |
string | VM name |
total_storage_tib |
number | Total storage in TiB |
used_storage_tib |
number | Used storage in TiB |
vcpus |
integer | Number of vCPUs |
memory_size_gib |
number | Memory in GiB |
protection_type |
boolean | Whether VM has protection policy |
power_state |
boolean | true = on, false = off |
created_at |
string | ISO 8601 timestamp |
Get per-VM costs
GET /vm-data/costs
Returns per-VM cost breakdown for a given month, calculated using the same Average Usage billing method as the billing system. Each VM's resource usage is averaged across batch snapshots for the month, then priced using the organisation's configured pricing.
Filters:
| Parameter | Type | Default | Description |
|---|---|---|---|
month |
integer | Current month | Month number (1–12). Out of range returns 400 |
year |
integer | Current year | Year (for example 2026) |
organisation_id |
integer | Token owner | Numeric id. Required for an admin token. An org token may pass its own id or a direct child's id |
For past months, the cost reflects the full month. For the current month, it is prorated to the current day.
Response structure:
{
"data": [ ... ],
"summary": {
"organisation_id": "C729",
"organisation_name": "Acme Corp",
"month": 4,
"year": 2026,
"days_in_month": 30,
"days_elapsed": 8,
"total_vms": 12,
"billable_vms": 11,
"total_month_to_date_cost": 1234.56,
"total_full_month_estimate": 4567.89
}
}
Response fields (per VM):
| Field | Type | Description |
|---|---|---|
id |
integer | VM id for the VM operation endpoints (its newest record that month) |
current |
boolean | true when the VM exists now (as on the portal's VM list); false for a VM deleted since, which still has costs this month |
extId |
string | External VM identifier |
name |
string | VM name |
source_data_type |
string | "VMs" or "VMs DR" |
vcpus |
integer | Current vCPU count |
memory_size_gib |
number | Current memory in GiB |
total_storage_tib |
number | Current total storage in TiB |
used_storage_tib |
number | Current used storage in TiB |
power_state |
boolean | true = on, false = off |
protection_type |
boolean | Whether VM has protection policy |
guest_os_license |
string | OS license name (nullable) |
excluded_from_billing |
boolean | true if excluded from billing calculations |
cost_breakdown |
array | Itemised costs (see below) |
month_to_date_cost |
number | Prorated cost incurred so far this month |
full_month_estimate |
number | Estimated cost for the full month |
cost_breakdown items:
| Field | Type | Description |
|---|---|---|
resource |
string | Resource name (e.g. "vCPU", "Memory GiB") |
quantity |
number | Average quantity for the month |
unit_price |
number | Price per unit |
full_month_cost |
number | Full-month cost for this resource |
Billing rules reflected in per-VM costs:
- Powered-off VMs are only billed for storage (not CPU/memory)
- DR VMs use DR-specific pricing (vCPU DR, Memory DR, Storage DR)
- Windows and SQL license costs are billed per vCPU
- Protected storage is billed at a separate rate
- Excluded VMs appear in the response (flagged) but are excluded from summary totals
- Self-service organisations (customer codes starting with
S) are priced from the shared self-service price list, not from price rows stored on that customer organisation
Deploy (create) a VM
POST /vm-data
Creates a new VM from a template. Requires an organisation token (admin tokens are rejected). Returns 202 Accepted with an operation_id; poll GET /operations/{operation_id} for progress. For self-service organisations a payment method and available VM quota are required.
Body parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
vm_name |
string | yes | 1–63 chars, starts/ends alphanumeric; letters, digits, -_. and spaces |
template_ext_id |
string | yes | A template template_ext_id from GET /vm-templates |
version_ext_id |
string | no | Template version; defaults to the template's active version |
vcpus |
integer | no | One of 1, 2, 4, 8; defaults to the template's value |
memory_gib |
integer | no | One of 1, 2, 4, 8, 16, 32, 64; defaults to the template's value |
disk_size_gib |
integer | no | One of 40, 60, 80, 100, 120, 160, 200, 250, 300, 400, 500; must be ≥ the template disk; defaults to the template disk |
deployment_preset_id |
integer | no | A region id from GET /deployment-presets; defaults to the default region |
ssh_key_id |
integer | no | An SSH key id from GET /ssh-keys (must belong to your org); injected via cloud-init |
Rate limited to 1 deployment per user per 60 seconds. Only one VM operation may be in progress at a time.
List deployable templates
GET /vm-templates
Lists templates available to your organisation. Optional query deployment_preset_id selects the region/source. Each item: template_ext_id, version_ext_id, version_name, name, description, vcpus, memory_gib, disk_gib, gc_override_enabled (cloud-init/SSH support).
List deployment regions (presets)
GET /deployment-presets
Lists regions that accept new deployments. Each item: id, name, is_default.
SSH keys (organisation token)
GET /ssh-keys
GET /ssh-keys/{id}
POST /ssh-keys
DELETE /ssh-keys/{id}
Keys belong to your organisation's user. Each key: id, name, type, fingerprint, public_key, created_at, last_used_at. Pass a key id as ssh_key_id when deploying a Linux VM: the key is installed through cloud-init and SSH password login is turned off.
Add a key: POST /ssh-keys with name (one line, at most 255 characters) and either:
public_key: the user's own OpenSSH public key line (ssh-ed25519,ssh-rsa, orecdsa-sha2-nistp256/384/521), for example the contents of~/.ssh/id_ed25519.pub. Prefer this when the user already has a key: the private key never leaves their machine.- nothing else: ASI generates an Ed25519 key pair. The 201 response includes
private_key(OpenSSH format). It is shown once and never stored. Give it to the user straight away and tell them to save it withchmod 600.
Limits: at most 10 keys, and one new key per 60 seconds (shared with the portal). Refusals add reason and retryable: 422 invalid_name or invalid_key, 409 duplicate (that public key is already on the account) or limit_reached, 429 throttled (retry after a minute).
Delete: DELETE /ssh-keys/{id}. VMs already deployed with the key keep it; new deployments can no longer use it.
VM operations
All operations require an organisation token. Resize, delete and restore require the VM to be powered off. Every state-changing operation returns 202 Accepted with an operation_id to poll via GET /operations/{operation_id}.
One change at a time. A VM accepts one operation at a time: while one is running (power, rename, resize, snapshot), any other change to that VM returns 409 with reason: vm_busy. Poll the operation_id until status is completed or failed before the next change, for example before powering a VM on after a resize.
{id} can be any of the VM's ids: the id from GET /vm-data/costs, from a /vm-data snapshot row, or one returned by an earlier call. The hourly sync adds a new row (and id) per VM, and every operation acts on the VM's newest row; responses return that row's id as vm_id. A VM that has been deleted returns 404 for all of its ids.
| Method & path | Description |
|---|---|
GET /vm-data/{id}/state |
Live power state and IP addresses |
POST /vm-data/{id}/power-on |
Power on |
POST /vm-data/{id}/power-off |
Power off |
POST /vm-data/{id}/rename |
Rename — body { "new_name": "..." } |
POST /vm-data/{id}/modify-resources |
Change vcpus, memory_gib, disk_size_gib (powered off) |
DELETE /vm-data/{id} |
Delete the VM (powered off) |
GET /vm-data/{id}/snapshots |
List snapshots (max 3 per VM) |
POST /vm-data/{id}/snapshots |
Create snapshot — optional { "snapshot_name": "..." } |
DELETE /vm-data/{id}/snapshots/{snapshotExtId} |
Delete a snapshot |
POST /vm-data/{id}/snapshots/{snapshotExtId}/restore |
Restore from a snapshot (powered off) |
3. Billings
Each sync writes a batch. A month has many batches (about one per day), and each batch has one line per priced resource type. amount is month-to-date. full_month_amount is the full-month projection. In the last batch of a closed month the two match.
To state a month's figure, group the month's lines by batch_id, sum full_month_amount in each batch, and take the largest batch. The latest batch can be smaller (an incomplete day, or a closed month whose peak was earlier).
An unpriced line (price is null) with quantity of exactly 1 is ambiguous. Unused resource types are stored as quantity 1, and a real quantity of 1 looks the same. Say it might be either. Do not call it zero usage.
List billings
GET /billings
Filters:
| Parameter | Type | Description |
|---|---|---|
month |
integer | Month number (1–12) |
year |
integer | Year (for example 2026) |
price_resource_type_id |
integer | Filter by resource type |
organisation_id |
integer | Numeric org id, when the token can read more than one |
Without month and year the list is every batch the token can see. Pass both, and per_page=100, before paging.
Get billing record
GET /billings/{id}
Response fields:
| Field | Type | Description |
|---|---|---|
id |
integer | Internal ID |
batch_id |
string | Processing batch identifier |
organisation_id |
integer | Numeric organisation id |
month |
integer | Billing month (1–12) |
year |
integer | Billing year |
price_resource_type_id |
integer | Resource type ID |
price_id |
integer | Price ID used for calculation |
quantity |
number | Quantity of resources |
amount |
number | Billed amount |
full_month_amount |
number | Full-month equivalent amount |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
organisation |
object | { id, name, organisation_id } |
price_resource_type |
object | { id, name } — resource type |
price |
object | { id, name } or null when the line has no price. The unit amount is on GET /prices/{id}, not on this object |
Billing summary (latest batch only)
GET /billings/summary
Sums amount and full_month_amount for the latest batch_id in each month, grouped by resource type. This is not the largest batch and not the invoice total. Label it as the latest batch.
Filters: year, year_from, year_to, month (1–12), and organisation_id (numeric id).
Response fields (per row): year, month, price_resource_type_id, resource_type_name, total_amount, total_full_month_amount, record_count.
There is no meta or links. The body is { "data": [ ... ] }.
4. Prices
Returns pricing configured for your organisation.
List prices
GET /prices
Filters:
| Parameter | Type | Description |
|---|---|---|
price_resource_type_id |
integer | Filter by resource type |
price_type_id |
integer | Filter by price type |
name |
string | Partial name match |
organisation_id |
integer | Numeric org id, when the token can read more than one |
Get price
GET /prices/{id}
Response fields:
| Field | Type | Description |
|---|---|---|
id |
integer | Internal ID |
organisation_id |
integer | Numeric organisation id |
price_resource_type_id |
integer | Resource type ID |
amount |
number | Price amount |
cost |
number | Cost amount |
min_qty |
number | Minimum quantity |
price_type_id |
integer | Price type ID |
name |
string | Price name |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
organisation |
object | { id, name, organisation_id } |
price_resource_type |
object | { id, name } — resource type |
price_type |
object | { id, name } — price type |
5. Price Resource Types
Reference data — the types of resources that can be priced.
List price resource types
GET /price-resource-types
Filters: name (partial match).
Get price resource type
GET /price-resource-types/{id}
Response fields:
| Field | Type | Description |
|---|---|---|
id |
integer | Internal ID |
name |
string | Display name |
description |
string | Description (nullable) |
internal_name |
string | Internal identifier |
order |
integer | Display order |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
6. Classifications
Reference data — VM classifications used for billing and reporting.
List classifications
GET /classifications
Filters: name (partial match).
Get classification
GET /classifications/{id}
Response fields: id, name, description, created_at, updated_at.
7. Guest Operating Systems
Reference data — guest OS types.
List guest OS
GET /guest-os
Filters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Partial name match |
family |
string | OS family: windows, linux, or macos |
architecture |
string | Architecture: 16, 32, or 64 |
Get guest OS
GET /guest-os/{id}
Response fields: id, family, architecture, name, created_at, updated_at.
8. Guest OS Licenses
Reference data — OS license types.
List guest OS licenses
GET /guest-os-licenses
Filters: name (partial match).
Get guest OS license
GET /guest-os-licenses/{id}
Response fields: id, name, created_at, updated_at.
9. Source Data Types
Reference data — types of data collected from cloud providers.
List source data types
GET /source-data-types
Filters: name (partial match).
Get source data type
GET /source-data-types/{id}
Response fields: id, name, description, created_at, updated_at.
10. Sources (admin token only)
GET /sources
GET /sources/{id}
Organisation tokens receive 403 This endpoint is not available for organisation tokens. Do not call it with a customer token.
The response is id, name, base_url, enabled, source_type, and organisation. It does not include passwords, API keys, or tokens.
11. Operation status
GET /operations/{operationId}
Poll an async VM operation started by deploy or another 202 response. The id is operation_id from that response. An organisation token can read only its own org's operations. Unknown or expired ids return 404.
Response fields: operation_id, status (queued, deploying, completed, failed, and similar in-progress values), operation_type, vm_name, description, started_at, error (null until a failure).
12. S3 Buckets
Self-service object storage. Each bucket has its own access key that works only for that bucket. Buckets accept connections from New Zealand IP addresses only. Use path-style addressing (rclone force_path_style = true, AWS CLI addressing_style = path) against the bucket's endpoint.
Every call runs straight away (no operation to poll). One S3 change per organisation runs at a time; a second one gets 409 busy.
List buckets
GET /s3-buckets
Offset pages. Filters: organisation_id (numeric), name (partial match). Reads work with both token types.
Get bucket
GET /s3-buckets/{id}
Response fields: id, organisation_id (numeric), name, access_key_id, permission_groups, allowed_cidrs (empty means no IP restriction beyond New Zealand), endpoint, addressing_style (path), created_at, updated_at. Never includes a secret.
Create bucket (organisation token)
POST /s3-buckets
{"name": "backups", "permission_groups": ["read", "write"]}
name(required): lowercase letters, numbers and hyphens. The organisation prefix (for examples001-) is added if missing, sobackupsands001-backupsboth creates001-backups. Names are unique across the platform; the full name is 3–63 characters.permission_groups(optional, default all):read,write,delete,retention(retention and legal hold),restore. At leastreadorwrite. Listing the bucket is always allowed.allowed_cidrs(optional): public IPv4/32s, at most 20. Only while IP allowlists are enabled; otherwise 422.- Needs a payment method on the account (422 otherwise) and stays within the organisation's bucket limit (409
limit_reached).
Returns 201 with data (the bucket) and credentials: access_key_id, secret_access_key, endpoint, addressing_style. The secret is shown once.
Rotate key (organisation token)
POST /s3-buckets/{id}/rotate-key
Issues a new key and deletes the current one, which stops working immediately. Same credentials block as create. Use it when the user has lost the secret.
Change permissions (organisation token)
PUT /s3-buckets/{id}/permissions
{"permission_groups": ["read"]}
Replaces the groups. The current key's rights change immediately; no new secret.
Change IP allowlist (organisation token)
PUT /s3-buckets/{id}/allowlist
{"allowed_cidrs": ["203.0.113.10/32"]}
Replaces the list; [] removes the restriction. It does not lift the New Zealand-only restriction. Returns 404 while IP allowlists are not enabled.
Delete bucket (organisation token)
DELETE /s3-buckets/{id}
The bucket must be empty (409 bucket_not_empty otherwise). Cannot be undone.
Empty and delete bucket (organisation token)
POST /s3-buckets/{id}/empty-and-delete
{"confirm_name": "s001-backups"}
Permanently deletes every object, old version and unfinished upload, then the bucket. confirm_name must equal the full bucket name. Confirm with the user before calling. One call empties for about 30 seconds: a large bucket returns 409 empty_incomplete with retryable: true, and calling again carries on. Objects under retention or legal hold are never removed (409 objects_locked).
S3 errors
Failures from object storage add reason and retryable to the usual body:
{"error": "Conflict", "reason": "bucket_taken", "message": "...", "retryable": false}
| Status | reason |
|---|---|
| 422 | invalid_name, invalid_permissions, invalid_allowlist |
| 409 | bucket_taken, limit_reached, busy, bucket_not_empty, empty_incomplete, objects_locked, account_conflict, key_limit |
| 503 | upstream |
Retry only when retryable is true (busy, empty_incomplete, upstream). The message is safe to show the user.
13. Firewall Rules (port forwarding)
Forward a port on the organisation's public IP to a VM. Rules are per region: pass deployment_preset_id (from GET /deployment-presets) or leave it out for the default region. Organisation token for a self-service organisation; only that organisation's own rules. The network exists once a VM has been deployed in the region. Changes apply on the firewall straight away (no operation to poll).
List rules
GET /firewall-rules?deployment_preset_id=3
meta: region, network_provisioned, public_ip, subnet, gateway_ip, max_rules. Each rule: name (listed form, lowercase with hyphens), protocol, external_port, internal_ip, internal_port, source_addresses (empty = any address), expires_at (null = never), connect_to (public_ip:external_port). With no network yet, network_provisioned is false and data is empty.
Add a rule
POST /firewall-rules
{"name": "ssh", "external_port": 22022, "internal_ip": "172.22.0.10", "internal_port": 22,
"source_addresses": ["203.0.113.10/32"], "expires_in": "1h"}
name: letters, numbers, hyphens, underscores, spaces; at most 32 characters. It is listed lowercase with hyphens (Web Server→web-server); the response returns that listed name.protocol:tcp(default) orudp.internal_ip: the VM's address fromGET /vm-data/{id}/state; must be inmeta.subnetand not the gateway.source_addresses(optional): IPv4 addresses or CIDRs allowed to connect, at most 20. Prefer restricting to the user's own IP for SSH and admin ports.expires_in(optional):never(default),1h,4h,24h,7d. The rule is removed automatically after that.- At most 50 rules per region. An external port + protocol, an internal IP + port + protocol, and a listed name can each be used once.
Returns 201 with the rule, including connect_to.
Delete a rule
DELETE /firewall-rules/{name}?deployment_preset_id=3
name is the listed name. Traffic to the port stops straight away.
Firewall errors
Refusals add reason (and field for input problems) and retryable:
| Status | reason |
|---|---|
| 422 | invalid_name, invalid_port, invalid_internal_ip, invalid_source_addresses, invalid_expiry, invalid_protocol |
| 409 | network_not_provisioned (deploy a VM in the region first), region_not_ready, limit_reached, duplicate_port, duplicate_destination, duplicate_name, ambiguous, busy |
| 404 | not_found |
| 503 | upstream |
Retry only when retryable is true (busy, upstream).
Make a new Linux VM reachable over SSH
POST /ssh-keys(or upload the user's key), thenPOST /vm-datawithssh_key_id.- Poll
GET /operations/{operation_id}untilstatusiscompleted(a 404 there means it finished and the record expired). Find the VM'sidinGET /vm-data/costs(matchname), then callGET /vm-data/{id}/stateuntilip_addressesis not empty. POST /firewall-rulesfrom a free external port to that IP on port 22, limited to the user's IP.- Connect:
ssh -i <key> -p <external_port> asi@<public_ip>(the key's user is the template's cloud-init user, normallyasi).
14. AI Inference
Pay-per-token LLM access for self-service organisations: turn it on, see models and prices, and manage the API keys used to call the inference endpoints. Organisation token for a self-service organisation; only that organisation. Every endpoint returns 404 while ASI does not offer AI Inference.
Status
GET /ai-inference
data: enabled, blocked (requests refused by ASI), can_enable, enable_blocker (reason and message, or null), base_urls (openai for OpenAI-compatible clients, anthropic for Anthropic-compatible clients), limits (requests_per_minute, tokens_per_minute; null until turned on), currency (NZD), prices_exclude_gst, models (slug to send as model, name, context_window, status, price_per_million_tokens with input, cached_input, output), max_active_keys. Before it is turned on, models shows what turning it on would give.
Turn on / off
POST /ai-inference/enable
POST /ai-inference/disable
Turning it on starts per-token billing to the card on file. Confirm with the user first. It needs a payment method (422 payment_method_required). If ASI turned it off, only ASI can turn it back on (409 disabled_by_asi). Turning it off stops every key within a few seconds; keys and usage are kept, usage so far this month is still billed, and it can be turned back on.
Keys
GET /ai-inference/keys
GET /ai-inference/keys/{id}
POST /ai-inference/keys {"name": "Claude Code", "expires_on": "2026-12-31"}
POST /ai-inference/keys/{id}/revoke
DELETE /ai-inference/keys/{id}
- Each key:
id,name,prefix,status(active,expired,revoked),created_at,expires_at,last_used_at(updated hourly),revoked_at,revoked_by_asi. - Create:
name1 to 100 characters;expires_on(optional, YYYY-MM-DD) means the key works to the end of that day, New Zealand time. The 201 response holdssecret, shown once and never stored. Give it to the user straight away; suggest they keep it in an environment variable (for exampleASI_INFERENCE_KEY), not in code. At most 20 active keys (409limit_reached). - Revoke: anything using the key stops within a few seconds. Cannot be undone.
- Delete: only a revoked or expired key (409
must_revoke_first). It leaves the list; its usage stays on the bills.
Call the models with the secret as the API key (Bearer token): OpenAI-compatible clients at base_urls.openai (for example POST {openai}/chat/completions with "model": "<slug>"), Anthropic-compatible clients at base_urls.anthropic.
AI Inference errors
Refusals add reason and retryable:
| Status | reason |
|---|---|
| 422 | invalid_name, invalid_expiry, payment_method_required |
| 409 | not_enabled, disabled_by_asi, limit_reached, must_revoke_first, no_prices |
| 403 | not_self_service |
| 503 | upstream (retryable) |
Usage and cost are billed as AI Inference lines on /billings.
Error Responses
| Status | Meaning | Example |
|---|---|---|
| 400 | Bad request | {"error":"Bad Request","message":"Month must be between 1 and 12."} or admin token omitted organisation_id on /vm-data/costs |
| 401 | Unauthorized | {"error": "Unauthorized", "message": "Invalid or missing bearer token"} |
| 403 | Forbidden | Admin token on a write, or an organisation token on GET /sources |
| 404 | Not found | {"error": "Not Found", "message": "VM data not found"} |
| 409 | Conflict | A VM operation is already in progress |
| 422 | Unprocessable | page > 1 on /vm-data, VM not on Nutanix, or VM must be powered off |
| 429 | Rate limited | {"message":"Too Many Attempts."} for the 60/minute limit. Deploy uses {"error":"Too Many Requests",...} |
| 503 | Unavailable | Template or live VM state could not be retrieved |
Example Requests
List all powered-on VMs
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data?power_state=true"
Get billing for March 2026
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/billings?month=3&year=2026"
List VMs with at least 4 vCPUs and 8 GiB memory
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data?vcpus_min=4&memory_size_gib_min=8"
Follow the VM snapshot cursor (do not send page)
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data?per_page=50"
# then follow links.next, or:
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data?per_page=50&cursor={{CURSOR}}"
Latest billing batch for one month (not the invoice total)
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/billings/summary?month=3&year=2026"
Get change history for a VM
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data/42/history"
Get per-VM costs for current month
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data/costs"
Get per-VM costs for a previous month
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-data/costs?month=3&year=2026"
Deploy a minimal VM
# 1. Get available templates
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/vm-templates"
# 2. Deploy — vm_name is required (not "name"); use template_ext_id from above
curl -X POST \
-H "Authorization: Bearer {{API_TOKEN}}" \
-H "Content-Type: application/json" \
-d '{
"vm_name": "myvm01",
"template_ext_id": "{{TEMPLATE_EXT_ID}}",
"vcpus": 1,
"memory_gib": 4
}' \
"https://cloud.asi.nz/api/v1/vm-data"
# 3. Poll until complete
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/operations/{{OPERATION_ID}}"
Note: The body field is
vm_name(notname). The name must be 1–63 characters, start and end with alphanumeric, and contain only letters, digits, hyphens, underscores, periods, and spaces.
Get your organisation's pricing
curl -H "Authorization: Bearer {{API_TOKEN}}" \
"https://cloud.asi.nz/api/v1/prices"
Tips for Querying
- Read scope — an organisation token sees its org and direct children. Filter with the numeric
organisation_idwhen you need one of them. Writes stay on the token owner. - Page with
per_page=100on offset lists and followlinks.next. On/vm-data, follow the cursor. Never sendpagegreater than 1 to/vm-data. - Combine filters —
?power_state=true&vcpus_min=4&memory_size_gib_min=8on/vm-data. - Check
meta.totalon offset lists before walking every page./vm-datahas no total. - Reference data (price-resource-types, classifications, guest-os, guest-os-licenses, source-data-types) is a small lookup. Fetch it once.
- Timestamps are ISO 8601 in the Pacific/Auckland timezone.
- Boolean values accept
true/falseor1/0. - Storage units:
total_storage_tibandused_storage_tibare tebibytes (TiB). Memory (memory_size_gib) is gibibytes (GiB). - Per-VM costs —
/vm-data/costsis one row per VM for that month. It is an estimate. It does not include backup, object storage, or fixed charges, so it is not the invoice. S3 bucket usage is billed from/billings(S3 lines), not from/s3-buckets. Official billing is/billings, using the largest batch. - Do not dedupe
/vm-datapage 1. Those rows are hourly snapshots ordered by name, and a cursor page is not "the latest of every VM". Current inventory is/vm-data/costs. Live power and IPs areGET /vm-data/{id}/state(422 when the VM is not on Nutanix).