VIIPER provides a lightweight TCP API for managing and controlling virtual buses/devices.
It's designed to be trivial to drive from any language that can open a TCP socket and send null-byte-terminated payloads.
!!! tip "Client Libraries Available"
Client libraries are available that abstract away any protocol details described below.
For most use cases, you should use one of the provided client libraries rather than implementing the raw protocol yourself:
- [Go Client](../clients/go.md): Reference implementation included in the repository
- [Generator Documentation](../clients/generator.md): Information about code generation
- [C++ Client Library](../clients/cpp.md): Header-only C++20 library (requires external JSON parser)
- [C# Client Library](../clients/csharp.md): Generated .NET library with async/await support
- [TypeScript Client Library](../clients/typescript.md): Generated Node.js library with EventEmitter streams
- [Rust Client Library](../clients/rust.md): Generated Rust library with sync/async support
The documentation below is provided for reference and for implementing clients in languages not supported officially.
The TCP API is inspired by the ubiquitous HTTP REST style, but is more lightweight.
If you ever worked with HTTP APIs before, you'll feel right at home.
The exception to this are the device-control and feedback streams, which are raw binary streams specific to each device type.
- Transport: TCP with optional encryption (ChaCha20-Poly1305)
- Default listen address:
:3242(configurable via--api.addr) - Authentication: Required for remote connections, optional for localhost (password-based with HMAC validation)
- Encryption: Automatic for authenticated connections (ChaCha20-Poly1305 with unique session keys)
- Request format: a single ASCII/UTF‑8 line terminated by
\0 - Routing: path followed by optional payload separated by whitespace
(e.g.,bus/list\0orbus/create 5\0) - Payload: optional string that can be a JSON object, numeric value, or plain string depending on the endpoint.
The payload may contain newlines (e.g., pretty-printed JSON) as only the null byte terminates the request. - Success response: a single line containing a JSON payload (or an empty line for commands that have no payload), terminated by connection close
- Error response: a single line JSON object following RFC 7807 Problem Details format with a
statusfield (HTTP-style status code) and other error details, terminated by connection close
!!! tip "Testing the API"
For quick testing, you can use tools like netcat (Linux/macOS) or PowerShell scripts (Windows) to send requests and read responses.
!!! warning "Connection timing and auto‑cleanup"
After you add a device with bus/{id}/add, you must connect to its streaming endpoint within the configured DeviceHandlerConnectTimeout (default: 5s). If no stream connection is established in time, the device is automatically removed. Likewise, when a stream disconnects, a reconnection timer with the same timeout starts; if the client doesn’t reconnect before it expires, the device is removed.
!!! warning "Authentication Required for Remote Connections" VIIPER requires authentication for all non-localhost connections.
- **Localhost clients** (`127.0.0.1`, `::1`, `localhost`): Authentication is **optional** (but supported) by default
- **Remote clients**: Authentication is **required** and enforced
On first start, VIIPER generates a random password
and saves it to `<USER_CONFIG_DIR>/viiper.key.txt`.
Windows: `%APPDATA%\VIIPER\viiper.key.txt`
Linux (user): `~/.config/github.com/Alia5/viiper/viiper.key.txt`
Linux (root/systemd): `/etc/viiper/viiper.key.txt`
Remote clients must provide this password to establish a connection.
See the [Configuration](../cli/configuration.md) documentation for details on password management and the `--api.require-localhost-auth` option.
!!! info "null byte excluded"
The \0 (null byte) terminator is excluded from all examples below for readability.
All requests must be terminated with a null byte (unless otherwise noted).
-
Bus Management
Create, list, and remove virtual buses
-
Device Management
Add, list, and remove devices on a bus
-
Device Control / Feedback
Real-time input and feedback streams for devices
-
Error Handling
Error response format and common error codes
??? info "ping - Simple identity and version check"
Request: ping
**Response:** `{ "server": "VIIPER", "version": "1.2.3[-dev-abcd]" }`
??? info "bus/list - List all virtual bus IDs"
Request: bus/list
**Response:** `{ "buses": [1, 2, ...] }`
??? info "bus/create - Create a new bus"
Request: bus/create or bus/create 5
**Payload:** Optional numeric bus ID (e.g., `5`)
If provided, VIIPER attempts to create the bus with that id; otherwise it picks the next free id.
**Response:** `{ "busId": <id> }`
??? info "bus/remove - Remove a bus and all devices on it"
Request: bus/remove 1
**Payload:** Numeric bus ID (e.g., `1`)
**Response:** `{ "busId": <id> }`
??? info "bus/{id}/list - List devices on a bus"
Request: bus/1/list
**Response:**
```json
{
"devices": [
{
"busId": 1,
"devId": "1",
"vid": "0x045e",
"pid": "0x028e",
"type": "xbox360"
"deviceSpecific": {
"subType": 1
}
}
]
}
```
??? info "bus/{id}/add - Add a device to a bus"
Request: bus/1/add {"type":"xbox360"}
**Payload:** JSON object with device creation parameters
```json
{
"type": "<deviceType>",
"idVendor": <optional_vid>,
"idProduct": <optional_pid>,
"deviceSpecific": <optional device specific args>
}
```
**Examples:**
- `{"type":"xbox360"}`
- `{"type":"keyboard","idVendor":1234,"idProduct":5678}`
- `{"type":"xbox360", "deviceSpecific": {"subType": 7}}`
**Response:**
```json
{
"busId": 1,
"devId": "1",
"vid": "0x045e",
"pid": "0x028e",
"type": "xbox360",
"deviceSpecific": {
"subType":7
}
}
```
!!! warning "Connection timeout"
After add, the server starts a connect timer (default `5s`). You must open a device stream before the timeout expires, otherwise the device is auto-removed.
!!! info "Auto-attach"
If [auto-attach](../cli/server.md#api.auto-attach-local-client) is enabled (default), the server automatically attaches the new device to a local USBIP client on the same host (localhost only). Failures are logged but do not affect the API response.
??? info "bus/{id}/remove - Remove a device from a bus"
Request: bus/1/remove 1
**Payload:** Numeric device ID (e.g., `1` for device 1-1 on the bus)
**Response:** `{ "busId": <id>, "devId": "<dev>" }`
Device Control and Feedback requires an initial "handshake" request, afterwards the connection is used as a long-lived (device-specific, binary) bidirectional stream.
!!! info "Establish control/feedback connection/stream"
Path: bus/{busId}/{deviceId}
**Handshake:** Send the path followed by `\0` (null byte)
Example: `bus/1/1\0`
**Type:** Long-lived TCP connection
**Purpose:** Device-specific, bidirectional stream.
!!! warning "Timeout behavior"
When a stream ends, a reconnect timer is started.
If the client doesn't reconnect in time, the device is removed.
Device control and feedback is device-specific.
Each device type defines it's own packet formats.
In general the client (your code) sends sends binary input state packets to the VIIPER server and possibly receives binary feedback packets (rumble, keyboard leds, etc.) back.
Refer to the individual device documentation for details on packet formats and behavior.
All errors are inspired by HTTP REST APIs and are returned as single-line JSON objects in the style of RFC 7807 Problem Details.
The connection closes immediately after the error response.
If you have ever worked with HTTP APIs, the errors and status codes will feel familiar.
{
"status": 400,
"title": "Bad Request",
"detail": "missing payload"
}Fields:
status(number): HTTP-style status code indicating the error typetitle(string): Short, human-readable summary of the problemdetail(string): Explanation specific to this occurrence
Error codes are basically HTTP carbon copies:
| Status | Title | Cause | Example |
|---|---|---|---|
| 400 | Bad Request | Invalid request format, missing payload, or invalid JSON | Missing device type in bus/{id}/add, invalid busId format |
| 404 | Not Found | Resource does not exist | Bus ID not found, device ID not found |
| 409 | Conflict | Resource already exists or cannot be modified | Bus ID already exists, auto-attach failure |
| 500 | Internal Server Error | (Unhandled) Server-side error during operation | Failed to marshal response, device add failure, unknown error |
=== "PowerShell (Windows)"
VIIPER includes convenience scripts for quick testing and automation:
```powershell
# Source the script to load helper functions
. .\scripts\viiper-api.ps1
# Use the Invoke-ViiperApi function (or 'viiper' alias)
viiper "bus/list"
viiper "bus/create"
viiper "bus/1/add {\"type\":\"xbox360\"}" -Port 3242 -Hostname localhost
```
The script provides `Invoke-ViiperApi` (alias: `viiper`) for sending commands and `Connect-ViiperDevice` for testing persistent device connections.
=== "netcat (Linux/macOS)""
```bash
# List buses
printf "bus/list\0" | nc localhost 3242
# Create a bus
printf "bus/create\0" | nc localhost 3242
# → {"busId":1}
# Create a bus with specific ID
printf "bus/create 5\0" | nc localhost 3242
# → {"busId":5}
# Add a virtual Xbox 360 controller to bus 1
printf 'bus/1/add {"type":"xbox360"}\0' | nc localhost 3242
# → {"busId":1,"devId":"1","vid":"0x045e","pid":"0x028e","type":"xbox360"}
# List devices on bus 1
printf "bus/1/list\0" | nc localhost 3242
```
Then, open a second TCP connection for device control to bus/1/1 (the API port, not the USBIP port).
First send the "handshake" bus/1/1\0,
then you'll write device-specific input packets and read device-specific feedback packets.
Any language with raw TCP support works.
package main
import (
"fmt"
"io"
"net"
)
func main() {
conn, _ := net.Dial("tcp", "localhost:3242")
defer conn.Close()
// Send request with null terminator
fmt.Fprint(conn, "bus/create\x00")
// Read entire response until connection closes
resp, _ := io.ReadAll(conn)
fmt.Println(string(resp)) // {"busId":1}\n
}For a higher-level experience, see the Go client in /apiclient/.
The VIIPER API controls which virtual devices exist and exposes a device stream for live input/feedback.
Separately, the USBIP server (default :3241) makes these devices attachable from USBIP clients.
Typical flow:
- Create a bus
- Add a device
- Connect the device stream
- Attach a USBIP client to the device
If auto-attach is enabled step 4 is attempted automatically for the local host; you still must perform step 3 to keep the device alive.