Using the Kyun API

Kyun provides HTTP API access for every user. The public API is the same API that’s used for the Kyun frontend, so everything you can do through the website, you can also do directly through the API.

https://api.kyun.sh/endpoints provides a list of all API endpoints, schemas and usage examples.

There are currently no official Kyun API libraries available. If you prefer having typed helper functions instead of calling the HTTP endpoints directly, you can use openapi-generator with the OpenAPI schema (available here).

Rate Limits

There is a global 100 requests/minute limit, tracked by either the IP address (if not authenticated) or authentication token/API key.

Getting an Authentication Token

If you want to call endpoints that require being logged into an account, you need to provide an authentication token via the HTTP header x-auth-token.

There are 2 types of tokens you can use:

For most usecases, using an API key is recommended.

2FA provides additional security in this case. The API key does not provide access to 2FA protected endpoints (such as logging in, generating agent tokens or changing the password) without an one-time passcode.

Node Agent

The Kyun backend has 2 parts: the main API server, which provides most functionality, and the node agent.

The node agent runs on each node (physical server) listening on port 1337, and provides public endpoints for video/text consoles, mounting ISOs and doing cloud-init (quick) installs.

All communication is done via WebSockets.

To call these endpoints, you need a one-time token associated with the VM that can be generated by calling GET /services/danbo/{id}/agentToken. This token needs to be passed as an URL query, e.g. wss://osaka.ro.kyun.network:1337/vnc?token={token}.

The node hostname (e.g. osaka.ro.kyun.network) is available in the Danbo data model, which you can get by calling GET /services/danbo/{id}.

/serial

Provides a rudimentary Serial (text) terminal.

All messages (input and output) are passed to/from the terminal without any transformations.

Compatible with xterm.js and similar TTY implementations.

/vnc

Provides raw VNC access. Compatible with noVNC and any other VNC client. The VNC password is the first 8 characters of the token.

/cloudInit

Downloads and installs a cloud-init enabled image (.img, .qcow2, any other format supported by QEMU) from an URL.

Pass the image URL as a query, e.g. wss://osaka.ro.kyun.network:1337/cloudInit?token={token}&imageUrl={encodedImageUrl}

The server will send progress/error messages.

Example parsing implementation in JavaScript:

if (data.startsWith('!ERR')) throw new Error(data.slice(4))
else if (data.startsWith('!PR'))
    const progress = Number(data.slice(3))
else {
    const stageNumber = data.match(/^(\d+)/)?.[1]
    const stageMessage = data.slice(String(stageNumber).length)
}

/downloadIso

Downloads and mounts an ISO file from an URL. Maximum filesize is 10 GB.

Pass the ISO URL as a query, e.g. wss://osaka.ro.kyun.network:1337/uploadIso?token={token}&isoUrl={encodedIsoUrl}.

The server will send progress/error messages, in the same format as /cloudInit.

/uploadIso

Allows you to upload and mount an ISO. This is a complex endpoint that will most likely be rewritten in the future. Maximum filesize is 10 GB.

Once the WS connection is established, the server will send gib. Whenever you receive gib, you must send a 1 MB raw binary chunk of the file, sequentially. When the file is fully uploaded, send EOF.

Here is an example implementation in JavaScript:

const ws = new WebSocket(`wss://osaka.ro.kyun.network:1337/uploadIso?token=${token}`)

const chunkSize = 1024 * 1024

let offset = 0

function sendNextChunk() {
    // https://developer.mozilla.org/en-US/docs/Web/API/File
    if (offset >= file.size) {
        ws.send('EOF')
        return
    }

    const chunk = file.slice(offset, offset + chunkSize)
    offset += chunkSize

    const reader = new FileReader()
    reader.onload = (e) => {
        ws.send(e.target.result)
    }
    reader.readAsArrayBuffer(chunk)
}

await new Promise((res, rej) => {
    ws.addEventListener('message', (event) => {
        if (event.data === 'gib') {
            sendNextChunk()
        } else rej(event.data)
    })

    ws.addEventListener('close', (e) => {
        if (e.code !== 1000) return rej()
        res()
    })
})