Overview
The TinyPilot REST API enables clients to create custom integrations for their TinyPilot device.
Clients can use the REST API to interact with their TinyPilot device independently of TinyPilot's native web interface.
Requirements
The TinyPilot REST API requires an TinyPilot Enterprise license for each API-accessible device.
Contact enterprise@tinypilotkvm.com to purchase an Enterprise license.
Activation
To activate your Enterprise license key and enable the REST API, run the following commands:
LICENSE_KEY="YOUR-LICENSE-KEY"
sudo su tinypilot bash -c \
"/opt/tinypilot/scripts/activate-license ${LICENSE_KEY}"
Endpoints
Authentication token
POST /api/v1/auth
Retrieve an API token for interacting with the REST API.
Tokens remain valid until the next restart of the TinyPilot server process.
Clients can request multiple tokens. Requesting a new token does not invalidate any previous tokens.
Note: The authentication token API does not support password-based authentication. When TinyPilot is configured to require a username and password for access, the REST API is inaccessible. TinyPilot requires an authentication token even in passwordless mode to prevent CSRF attacks.
Headers
No additional headers are required, but the request must not include an Origin
header.
Returns
On success, returns status code 200
with a HTTP body of a TinyPilot API token as a JSON object. The object contains a single field, token
that contains a string value.
Example: Retrieve API token
POST /api/v1/auth HTTP/1.1
Host: tinypilot
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "4e77f593-a8e0-4262-8a32-911110087060"
}
Screenshot
GET /api/v1/screenshot
Retrieve the current image on the target computer's display output.
Headers
Authorization
(required): A TinyPilot API token obtained from/api/v1/auth
in the formatBearer [TOKEN]
Returns
On success, returns status code 200
with a HTTP body of the current image on TinyPilot's remote screen as a JPEG image.
If TinyPilot is not receiving video input from the target computer, returns status code 204
and an empty response body.
Example: Retrieve current screenshot
GET /api/v1/screenshot HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 13845
[binary JPEG data]
Keystroke
POST /api/v1/keystroke
Generate a keystroke on the target computer.
Headers
Authorization
(required): A TinyPilot API token obtained from/api/v1/auth
in the formatBearer [TOKEN]
Content-Type
(required): Must beapplication/json
Request Body
A JSON object representing a keystroke:
Field | Default | Description |
---|---|---|
code |
none | (required) A string representation of the keyboard key to forward to the remote computer, using JavaScript KeyboardEvent.code constants. |
shiftLeft |
false |
A boolean representing whether the left Shift modifier key should be pressed during the keystroke. |
shiftRight |
false |
A boolean representing whether the right Shift modifier key should be pressed during the keystroke. |
altLeft |
false |
A boolean representing whether the left Alt modifier key should be pressed during the keystroke. |
altRight |
false |
A boolean representing whether the right Alt modifier key should be pressed during the keystroke. |
ctrlLeft |
false |
A boolean representing whether the left Ctrl modifier key should be pressed during the keystroke. |
ctrlRight |
false |
A boolean representing whether the right Ctrl modifier key should be pressed during the keystroke. |
metaLeft |
false |
A boolean representing whether the left Meta modifier key ("Windows key", "OS key") should be pressed during the keystroke. |
metaRight |
false |
A boolean representing whether the right Meta modifier key ("Windows key", "OS key") should be pressed during the keystroke. |
Returns
200
on success with an empty response body.400
if the client request was malformed.500
if the server failed to forward the keystroke to the target computer.
Example: Type text
The following request sequences causes TinyPilot to type Hi!<Enter>
on a target system configured for an en-US keyboard.
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"code": "KeyH",
"shiftLeft": true
}
HTTP/1.1 200 OK
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"code": "KeyI"
}
HTTP/1.1 200 OK
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"code": "Digit1",
"shiftRight": true
}
HTTP/1.1 200 OK
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"code": "Enter"
}
HTTP/1.1 200 OK
Example: Send Ctrl+Alt+Del
The following exchange shows the caller sending a Ctrl+Alt+Del key sequence to the remote computer:
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"code": "Delete",
"ctrlLeft": true,
"altRight": true
}
HTTP/1.1 200 OK
Example: Keystroke forwarding failure
The following exchange shows the caller sending a keystroke to a TinyPilot device that is disconnected from the target computer:
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"code": "KeyA"
}
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Failed to forward keystroke: could not access /dev/hidg0 (is
cable connected?)
Mouse Event
POST /api/v1/mouseEvent
Generate a mouse event on the target computer to move the mouse cursor and/or to issue mouse clicks.
Headers
Authorization
(required): A TinyPilot API token obtained from/api/v1/auth
in the formatBearer [TOKEN]
Content-Type
(required): Must beapplication/json
Request Body
A JSON object representing a mouse event:
Field | Default | Description |
---|---|---|
buttons |
none | (required) An integer representing which mouse buttons are pressed (see JavaScript MouseEvent.buttons ). |
relativeX |
none | (required) A decimal between 0.0 and 1.0 representing the mouse's relative x-offset from the left edge of the screen. |
relativeY |
none | (required) A decimal between 0.0 and 1.0 representing the mouse's relative y-offset from the top edge of the screen. |
verticalWheelDelta |
none | (required) An integer -1 , 0 , or 1 representing movement of the mouse's vertical scroll wheel. |
horizontalWheelDelta |
none | (required) An integer -1 , 0 , or 1 representing movement of the mouse's horizontal scroll wheel. |
Returns
200
on success with an empty response body.400
if the client request was malformed.500
if the server failed to forward the mouse event to the target computer.
Example: Move mouse
The following sequence of requests moves the mouse to the 4 corners of the screen on the target system, starting in the top-left corner and moving clock-wise:
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 0,
"relativeX": 0.0,
"relativeY": 0.0,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 200 OK
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 0,
"relativeX": 1.0,
"relativeY": 0.0,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 200 OK
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 0,
"relativeX": 1.0,
"relativeY": 1.0,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 200 OK
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 0,
"relativeX": 0.0,
"relativeY": 1.0,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 200 OK
Example: Left-click mouse
The following exchange performs a left-click of the mouse in the center of the screen on the target system:
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 1,
"relativeX": 0.5,
"relativeY": 0.5,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 200 OK
Example: Right-click mouse
The following exchange performs a right-click of the mouse in the center of the screen on the target system:
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 2,
"relativeX": 0.5,
"relativeY": 0.5,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 200 OK
Example: Mouse event forwarding failure
The following exchange shows the caller sending a mouse event to a TinyPilot device that is disconnected from the target computer:
POST /api/v1/mouseEvent HTTP/1.1
Host: tinypilot
Authorization: Bearer 4e77f593-a8e0-4262-8a32-911110087060
Content-Type: application/json
{
"buttons": 0,
"relativeX": 0.5,
"relativeY": 0.5,
"verticalWheelDelta": 0,
"horizontalWheelDelta": 0
}
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Failed to forward mouse event: could not access /dev/hidg1 (is cable connected?)
Sample script
The following is a Python script that exercises all the functionality of the TinyPilot REST API:
#!/usr/bin/env python3
"""Exercise all the functionality of the TinyPilot REST API.
This script performs the following actions:
1. Fetch an auth token.
2. Open a terminal on the target machine by pressing `Ctrl` + `Alt` + `T`.
3. Type `echo "Hello, World!"` and then press the `Enter` key on the target machine.
4. Open Google Chrome.
5. Navigate to https://jspaint.app.
6. Draw a rectangle in paint.
7. Save a screenshot of the display on the target machine.
"""
import json
import ssl
import sys
import time
import urllib.error
import urllib.request
BASE_URL = 'https://tinypilot/api/v1'
# Disable SSL verification
# This is not necessary if you install your TinyPilot device's CA
# certificate:
# https://tinypilotkvm.com/faq/fix-browser-privacy-errors
# Example:
# ctx = ssl.create_default_context(cafile='/path/to/tinypilot/ca.crt')
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def main():
# 1. Fetch an auth token
print('Fetching auth token...')
req = urllib.request.Request(f'{BASE_URL}/auth', method='POST')
try:
with urllib.request.urlopen(req, context=ctx) as response:
response_text = response.read().decode()
response_data = json.loads(response_text)
token = response_data['token']
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}', file=sys.stderr)
return
print('Fetching auth token done.')
# 2. Open a terminal by pressing `Ctrl` + `Alt` + `T`
print('Opening terminal...')
payload = {
'code': 'KeyT',
'ctrlLeft': True,
'altLeft': True,
}
req = urllib.request.Request(url=f'{BASE_URL}/keystroke',
method='POST',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
data=json.dumps(payload).encode())
try:
with urllib.request.urlopen(req, context=ctx) as response:
pass
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}', file=sys.stderr)
return
print('Opening terminal done.')
print('Waiting 3s for the terminal to open...')
time.sleep(3)
print('Waiting for the terminal to open done.')
# 3. Type `echo "Hello, World!"` then press `Enter`
print('Typing...')
payloads = [
{
'code': 'KeyE'
},
{
'code': 'KeyC'
},
{
'code': 'KeyH'
},
{
'code': 'KeyO'
},
{
'code': 'Space'
},
{
'code': 'Quote',
'shiftLeft': True
},
{
'code': 'KeyH',
'shiftLeft': True
},
{
'code': 'KeyE'
},
{
'code': 'KeyL'
},
{
'code': 'KeyL'
},
{
'code': 'KeyO'
},
{
'code': 'Comma'
},
{
'code': 'Space'
},
{
'code': 'KeyW',
'shiftLeft': True
},
{
'code': 'KeyO'
},
{
'code': 'KeyR'
},
{
'code': 'KeyL'
},
{
'code': 'KeyD'
},
{
'code': 'Digit1',
'shiftLeft': True
},
{
'code': 'Quote',
'shiftLeft': True
},
{
'code': 'Enter'
},
]
for payload in payloads:
req = urllib.request.Request(url=f'{BASE_URL}/keystroke',
method='POST',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
data=json.dumps(payload).encode())
try:
with urllib.request.urlopen(req, context=ctx) as response:
pass
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}',
file=sys.stderr)
return
print('Typing done.')
print('Waiting 1s for command to execute...')
time.sleep(1)
print('Waiting for command to execute done.')
# 4. Open Google Chrome
print('Typing...')
payloads = [
{
'code': 'KeyG',
},
{
'code': 'KeyO',
},
{
'code': 'KeyO',
},
{
'code': 'KeyG',
},
{
'code': 'KeyL',
},
{
'code': 'KeyE',
},
{
'code': 'Minus',
},
{
'code': 'KeyC',
},
{
'code': 'KeyH',
},
{
'code': 'KeyR',
},
{
'code': 'KeyO',
},
{
'code': 'KeyM',
},
{
'code': 'KeyE',
},
{
'code': 'Enter',
},
]
for payload in payloads:
req = urllib.request.Request(url=f'{BASE_URL}/keystroke',
method='POST',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
data=json.dumps(payload).encode())
try:
with urllib.request.urlopen(req, context=ctx) as response:
pass
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}',
file=sys.stderr)
return
print('Typing done.')
print('Waiting 1s for command to execute...')
time.sleep(3)
print('Waiting for command to execute done.')
# 5. Navigate to jspaint.app
print('Typing...')
payloads = [
{
'code': 'KeyL',
'ctrlLeft': True,
},
{
'code': 'KeyJ',
},
{
'code': 'KeyS',
},
{
'code': 'KeyP',
},
{
'code': 'KeyA',
},
{
'code': 'KeyI',
},
{
'code': 'KeyN',
},
{
'code': 'KeyT',
},
{
'code': 'Period',
},
{
'code': 'KeyA',
},
{
'code': 'KeyP',
},
{
'code': 'KeyP',
},
{
'code': 'Enter',
},
]
for payload in payloads:
req = urllib.request.Request(url=f'{BASE_URL}/keystroke',
method='POST',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
data=json.dumps(payload).encode())
try:
with urllib.request.urlopen(req, context=ctx) as response:
pass
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}',
file=sys.stderr)
return
print('Typing done.')
print('Waiting 1s for command to execute...')
time.sleep(5)
print('Waiting for command to execute done.')
# 6. Draw a rectangle in paint
payloads = [
{
'relativeX': 0.25,
'relativeY': 0.25,
'buttons': 0,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
{
'relativeX': 0.25,
'relativeY': 0.25,
'buttons': 1,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
{
'relativeX': 0.75,
'relativeY': 0.25,
'buttons': 1,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
{
'relativeX': 0.75,
'relativeY': 0.75,
'buttons': 1,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
{
'relativeX': 0.25,
'relativeY': 0.75,
'buttons': 1,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
{
'relativeX': 0.25,
'relativeY': 0.25,
'buttons': 1,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
{
'relativeX': 0.25,
'relativeY': 0.25,
'buttons': 0,
'verticalWheelDelta': 0,
'horizontalWheelDelta': 0,
},
]
for payload in payloads:
req = urllib.request.Request(url=f'{BASE_URL}/mouseEvent',
method='POST',
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
},
data=json.dumps(payload).encode())
try:
with urllib.request.urlopen(req, context=ctx) as response:
pass
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}',
file=sys.stderr)
return
print('Waiting 1s for command to execute...')
time.sleep(1)
print('Waiting for command to execute done.')
# 7. Save a screenshot of the display
print('Taking a screenshot...')
req = urllib.request.Request(url=f'{BASE_URL}/screenshot',
method='GET',
headers={'Authorization': f'Bearer {token}'})
try:
with urllib.request.urlopen(req, context=ctx) as response:
with open('screenshot.jpeg', 'wb') as f:
f.write(response.read())
except urllib.error.HTTPError as e:
print(f'HTTP Error {e.status}: {e.read().decode()}', file=sys.stderr)
return
print('Taking a screenshot done.')
if __name__ == '__main__':
main()