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.
These docs apply to TinyPilot Pro 3.2.0 and later. For earlier versions, see the legacy REST API docs.
Requirements
The TinyPilot REST API requires a TinyPilot Automation License for each API-accessible device.
Authentication and scope
As of TinyPilot Pro 3.2.0, the REST API uses persistent API keys. Keys work while web interface user authentication is enabled.
Activate license and create a key
- Open System > Automation in the TinyPilot web interface.

- Attach your Automation License.
- Create an API key. TinyPilot shows the key once; store it securely.
Revoke keys from the same dialog when you no longer need them. Deactivating the Automation License removes all API keys.

Authorize requests
Send the API key in the Authorization header:
Authorization: Bearer [API_KEY]
Note: POST /api/v1/auth no longer issues tokens. On 3.2.0 and later, that endpoint returns 410 Gone with JSON body EPHEMERAL_TOKEN_DEPRECATED. Use an API key from System > Automation instead.
Requests to protected endpoints without a valid API key return 403.
Allowed endpoints
A valid API key authorizes only the endpoints below. Existing endpoints outside this list return 403 and require a browser session (see the TinyPilot Web API). Unknown paths may return 404.
REST API (API key required)
GET /api/v1/screenshotPOST /api/v1/keystrokePOST /api/v1/mouseEventPOST /api/v1/paste
Status (available without an API key)
GET /api/statusGET /api/versionGET /api/network/status-
GET /api/settings/video(GET only) GET /state
Endpoints
Screenshot
GET /api/v1/screenshot
Retrieve the current image on the target computer's display output.
Headers
-
Authorization(required): A TinyPilot API key from System > Automation in the formatBearer [API_KEY]
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 [API_KEY]
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 key from System > Automation in the formatBearer [API_KEY] -
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
-
200on success with an empty response body. -
400if the client request was malformed. -
500if 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 [API_KEY]
Content-Type: application/json
{
"code": "KeyH",
"shiftLeft": true
}
HTTP/1.1 200 OK
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer [API_KEY]
Content-Type: application/json
{
"code": "KeyI"
}
HTTP/1.1 200 OK
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer [API_KEY]
Content-Type: application/json
{
"code": "Digit1",
"shiftRight": true
}
HTTP/1.1 200 OK
POST /api/v1/keystroke HTTP/1.1
Host: tinypilot
Authorization: Bearer [API_KEY]
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 [API_KEY]
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 [API_KEY]
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 key from System > Automation in the formatBearer [API_KEY] -
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
-
200on success with an empty response body. -
400if the client request was malformed. -
500if 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 [API_KEY]
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 [API_KEY]
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 [API_KEY]
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 [API_KEY]
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 [API_KEY]
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 [API_KEY]
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 [API_KEY]
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?)
Paste
POST /api/v1/paste
Paste text onto the target computer by sending the corresponding keystrokes.
The API sends keystrokes in a "fire and forget" manner, so a successful response does not guarantee that all keystrokes have reached the target computer by the time you receive the response. As a rule of thumb, wait about 100ms per character before assuming the pasted text has arrived.
Headers
-
Authorization(required): A TinyPilot API key from System > Automation in the formatBearer [API_KEY] -
Content-Type(required): Must beapplication/json
A JSON object representing the text to paste:
| Field | Default | Description |
|---|---|---|
text |
none | (required) A string of text to type on the target computer. |
language |
none |
(required) A string specifying the keyboard language as an IETF language tag. Supported languages are en-US, en-GB, and de-DE. The target computer's keyboard layout must match the language specified. |
Returns
-
200on success with an empty response body. -
400if the client request was malformed or contains characters that cannot be converted to keystrokes for the specified language.
Example: Paste text
POST /api/v1/paste HTTP/1.1 Host: tinypilot Authorization: Bearer [API_KEY] Content-Type: application/json { "text": "Hello, World!", "language": "en-US" }HTTP/1.1 200 OK
Example: Unsupported character
The following exchange shows the caller attempting to paste a character that cannot be represented as a keystroke for the specified language:
POST /api/v1/paste HTTP/1.1
Host: tinypilot
Authorization: Bearer [API_KEY]
Content-Type: application/json
{
"text": "こんにちは",
"language": "en-US"
}
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
These characters are not supported: 'こ', 'ん', 'に', 'ち', 'は'
Sample script
The following is a Python script that exercises all the functionality of the TinyPilot REST API. It assumes a Linux desktop on the target (for example, opening a terminal with Ctrl + Alt + T).
#!/usr/bin/env python3
"""Exercise all the functionality of the TinyPilot REST API.
This script performs the following actions:
1. Open a terminal on the target machine by pressing `Ctrl` + `Alt` + `T`.
2. Paste `echo "Hello, World!"` and then press the `Enter` key on the target machine.
3. Open Firefox.
4. Navigate to https://paint.js.org.
5. Draw a rectangle in paint.
6. Save a screenshot of the target machine display.
Create an API key in System > Automation, then set API_KEY below.
"""
import json
import ssl
import sys
import time
import urllib.error
import urllib.request
BASE_URL = 'https://tinypilot/api/v1'
API_KEY = '[API_KEY]'
# Seconds to wait after each keystroke to allow it to finish delivering
# before sending subsequent input.
SECONDS_PER_KEYSTROKE = 0.1
# 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():
# Use API key from System > Automation.
token = API_KEY
# 1. Open a terminal by pressing `Ctrl` + `Alt` + `T`.
print('Opening terminal...', end='', flush=True)
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
time.sleep(SECONDS_PER_KEYSTROKE)
print(' done.')
print('Waiting 3s for the terminal to open...', end='', flush=True)
time.sleep(3)
print(' done.')
# 2. Paste `echo "Hello, World!"` then press `Enter`.
print('Pasting text...', end='', flush=True)
text = 'echo "Hello, World!"'
payload = {
'text': text,
'language': 'en-US',
}
req = urllib.request.Request(url=f'{BASE_URL}/paste',
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
time.sleep(len(text) * SECONDS_PER_KEYSTROKE)
payload = {'code': 'Enter'}
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
time.sleep(SECONDS_PER_KEYSTROKE)
print(' done.')
print('Waiting 1s for command to execute...', end='', flush=True)
time.sleep(1)
print(' done.')
# 3. Open Firefox.
print('Opening Firefox...', end='', flush=True)
text = 'firefox'
payload = {
'text': text,
'language': 'en-US',
}
req = urllib.request.Request(url=f'{BASE_URL}/paste',
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
time.sleep(len(text) * SECONDS_PER_KEYSTROKE)
payload = {'code': 'Enter'}
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
time.sleep(SECONDS_PER_KEYSTROKE)
print(' done.')
print('Waiting 3s for command to execute...', end='', flush=True)
time.sleep(3)
print(' done.')
# 4. Navigate to paint.js.org.
print('Navigating to paint.js.org...', end='', flush=True)
payload = {
'code': 'KeyL',
'ctrlLeft': 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
time.sleep(SECONDS_PER_KEYSTROKE)
text = 'https://paint.js.org/'
payload = {
'text': text,
'language': 'en-US',
}
req = urllib.request.Request(url=f'{BASE_URL}/paste',
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
time.sleep(len(text) * SECONDS_PER_KEYSTROKE)
payload = {'code': 'Enter'}
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
time.sleep(SECONDS_PER_KEYSTROKE)
print(' done.')
print('Waiting 5s for command to execute...', end='', flush=True)
time.sleep(5)
print(' done.')
# 5. Draw a rectangle in paint.
print('Drawing rectangle...', end='', flush=True)
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(' done.')
print('Waiting 1s for command to execute...', end='', flush=True)
time.sleep(1)
print(' done.')
# 6. Save a screenshot of the display.
print('Taking a screenshot...', end='', flush=True)
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(' done.')
if __name__ == '__main__':
main()