Viewer API#
Your own scripts and tools can manage the sessions of a machine that runs Viewer: add sessions, change their proxies, start and stop them, read their status, and get told about changes as they happen. The API runs inside the Viewer app on that machine. It is off by default and needs a key.
Turn it on#
- Open
Settingsand setAPItoOn. Leave the port at37288unless another program uses it. - Click
Save. Viewer creates a key the first time. - Open
Settingsagain to copy theKeyand the address into your tool.
Add --enable-api when installing. The script prints the address and the key
at the end. Pass --api-key=<key> on the next run to keep the same key, and --api-port to change
the port.
The API answers on every address of the machine, so a tool on another machine uses the machine's IP
with the same port. Anyone holding the key can control the sessions of that machine: keep it private,
and make a new key (New key, then Save) if it leaks.
Quick start#
Every endpoint lives under http://127.0.0.1:37288 and needs the key in an Authorization header: Authorization: Bearer <key>. Reads are GET with parameters in the query; writes are POST with a JSON body.
Viewer must be open with the API turned on: Settings, API: On, Save (on Linux and Docker, --enable-api). The key and the address are in the same place.
The examples use curl in a POSIX shell (macOS, Linux, Git Bash). In Windows PowerShell call curl.exe, and put the JSON body in a file passed with -d @body.json.
Key and safety#
The key is 32 hex characters, one key for the endpoints below and the events WebSocket.
Send it in the Authorization header of every request: Authorization: Bearer <key>. A key in the URL is refused, except on the WebSocket, where browsers cannot set headers: there ?key= works too. A missing or wrong key answers 401 badKey.
The API listens on every address of the machine, so a tool on another machine reaches it with the machine's IP and the same port. The key is the only lock: anyone holding it can add, start, stop and delete the sessions of that machine. Keep it private; if it leaks, make a new key in Settings (New key, then Save), and give it to your tools.
Requests sent from a web page are always refused, so a site you visit cannot call the API.
Responses and errors#
Success is { "ok": true, "data": ... } with HTTP 200. A refusal of the whole request is { "ok": false, "code", "message", "details"? } with the status below. code never changes meaning; message is for people.
Endpoints that work on many sessions answer 200 and list the ids they could not act on in failed, each with id, code and message (next table).
| Code | HTTP | Meaning |
|---|---|---|
badKey |
401 | The key is missing or wrong. Send it as Authorization: Bearer <key>. |
invalidParams |
400 | A field is wrong; details lists each one with its path. |
invalidJson |
400 | The body is not valid JSON. |
unsupportedMediaType |
415 | A POST body was sent without Content-Type: application/json. |
notFound |
404 | No such endpoint, or no session with this id. |
methodNotAllowed |
405 | Wrong HTTP method for this endpoint. |
duplicate |
409 | Another session already uses this proxy, or the machine's own IP; details.id names it. Each IP can carry only one session. |
busy |
429 | Too many requests at once. Retry shortly. |
internal |
500 | Viewer hit an error. |
Per-session results#
Codes found in failed[] of start, stop, delete and export.
| Code | Meaning |
|---|---|
notFound |
No session with this id. |
System#
GET /v1/status#
What this Viewer is doing: version, sessions, totals, machine load. paired is whether an access key is set. hits and points are the totals of this run, as the toolbar shows them. loadavg is [0, 0, 0] on Windows.
No parameters.
Response
{
"ok": true,
"data": {
"app": {
"name": "9Hits Viewer",
"version": "6.0.1",
"build": "2026-10-18"
},
"api": {
"version": 1
},
"machine": {
"note": "vps-de-01",
"paired": true
},
"sessions": {
"total": 12,
"running": 10
},
"hits": 1480,
"points": 44400,
"uptimeSec": 86400,
"cpu": {
"loadavg": [
0.42,
0.38,
0.35
]
},
"ram": {
"total": 8589934592,
"available": 3221225472
}
}
}
Sessions#
GET /v1/sessions#
List sessions with their live status. Returns every session of this machine, newest first, like the table. hits and points are numbers; client is null until the session has an IP.
| Field | Type | Required | Description |
|---|---|---|---|
search |
string |
no | The same search as the box above the table: id, note, proxy, client IP, status, quality. |
running |
boolean |
no | Only running (true) or only stopped (false) sessions. |
type |
string |
no | Only sessions with this connection type. Values: system, http, socks4, socks5, ssh, exproxy. |
limit |
integer |
no | Min 1, max 1000. Default 500. |
offset |
integer |
no | Min 0. Default 0. |
Request
Response
{
"ok": true,
"data": {
"items": [
{
"id": "7f3a9c2e1b4d",
"note": "vps-de-01",
"proxy": {
"type": "http",
"server": "203.0.113.10:8080",
"user": "u1",
"password": "p1",
"exServer": ""
},
"client": {
"ip": "203.0.113.10",
"country": "DE"
},
"quality": {
"isProxy": true,
"speed": "good",
"performance": "medium",
"earnRate": 0.8
},
"hits": 42,
"points": 1260,
"status": "Viewing",
"statusColor": "green",
"running": true
}
],
"total": 1
}
}
GET /v1/sessions/{id}#
One session.
No parameters.
Request
Response
{
"ok": true,
"data": {
"id": "7f3a9c2e1b4d",
"note": "vps-de-01",
"proxy": {
"type": "http",
"server": "203.0.113.10:8080",
"user": "u1",
"password": "p1",
"exServer": ""
},
"client": {
"ip": "203.0.113.10",
"country": "DE"
},
"quality": {
"isProxy": true,
"speed": "good",
"performance": "medium",
"earnRate": 0.8
},
"hits": 42,
"points": 1260,
"status": "Viewing",
"statusColor": "green",
"running": true
}
}
POST /v1/sessions/create#
Add a session. Without proxy the session uses the machine's own IP (type system). A proxy another session already uses, or a second system session, answers duplicate. Pass start true to open it at once.
| Field | Type | Required | Description |
|---|---|---|---|
note |
string |
no | Free text shown in the note column. |
proxy |
object |
no | How the session reaches the internet. |
proxy.type |
string |
no | system is the machine's own IP (one session per machine); exproxy takes a proxy from a proxy pool (exServer). Required when creating. Values: system, http, socks4, socks5, ssh, exproxy. |
proxy.server |
string |
no | host:port for http, socks4, socks5 and ssh. user:pass@host:port and host:port:user:pass are split into user and password. SSH may omit the port (22). |
proxy.user |
string |
no | Proxy user name. |
proxy.password |
string |
no | Proxy password. |
proxy.exServer |
string |
no | exproxy only: the pool key or URL. Empty uses the 9Hits proxy pool. |
start |
boolean |
no | Start the session right after creating it. Default false. |
Request
curl -X POST "http://127.0.0.1:37288/v1/sessions/create" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"note": "vps-de-01",
"proxy": {
"type": "http",
"server": "u1:[email protected]:8080"
},
"start": true
}'
Response
POST /v1/sessions/{id}/update#
Change the note or proxy of a session. Only the fields sent change. proxy is merged over the current one: sending just server keeps the type, user and password. A running session uses the new proxy from its next view.
| Field | Type | Required | Description |
|---|---|---|---|
note |
string |
no | Free text shown in the note column. |
proxy |
object |
no | How the session reaches the internet. |
proxy.type |
string |
no | system is the machine's own IP (one session per machine); exproxy takes a proxy from a proxy pool (exServer). Required when creating. Values: system, http, socks4, socks5, ssh, exproxy. |
proxy.server |
string |
no | host:port for http, socks4, socks5 and ssh. user:pass@host:port and host:port:user:pass are split into user and password. SSH may omit the port (22). |
proxy.user |
string |
no | Proxy user name. |
proxy.password |
string |
no | Proxy password. |
proxy.exServer |
string |
no | exproxy only: the pool key or URL. Empty uses the 9Hits proxy pool. |
Request
Response
{
"ok": true,
"data": {
"id": "7f3a9c2e1b4d",
"note": "vps-de-01",
"proxy": {
"type": "http",
"server": "203.0.113.11:8080",
"user": "u1",
"password": "p1",
"exServer": ""
},
"client": {
"ip": "203.0.113.10",
"country": "DE"
},
"quality": {
"isProxy": true,
"speed": "good",
"performance": "medium",
"earnRate": 0.8
},
"hits": 42,
"points": 1260,
"status": "Viewing",
"statusColor": "green",
"running": true
}
}
POST /v1/sessions/import#
Add sessions in bulk, like the Import button. text is one proxy per line (host:port, host:port;user;pass, user:pass@host:port, optionally starting with the type: socks5;host:port;user;pass), or the JSON that export returns. JSON entries with an existing id update that session; proxy lines already used by a session or repeated in the text are skipped.
| Field | Type | Required | Description |
|---|---|---|---|
text |
string |
yes | Proxy lines or exported JSON. |
type |
string |
no | Type for lines that do not name one. Values: system, http, socks4, socks5, ssh, exproxy. Default "http". |
note |
string |
no | Note for every session created from a line without a type. |
Request
Response
GET /v1/sessions/export#
Sessions as a proxy list or as JSON, like the Export button. proxies gives one line per session with a proxy (system sessions are left out): type;server[;user][;password]. json gives id, note and proxy of every session, which import takes back.
| Field | Type | Required | Description |
|---|---|---|---|
format |
string |
no | Values: proxies, json. Default "proxies". |
ids |
string[] |
no | Only these sessions; all of them when absent. |
Request
Response
POST /v1/sessions/start#
Start sessions. Answers at once; the session reports its progress in status (see GET /v1/sessions and the session.status event). Sessions already running are listed in unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
no | Session ids. |
all |
boolean |
no | Every session, instead of ids. |
Request
Response
POST /v1/sessions/stop#
Stop sessions. Closes the browser of each running session. Sessions not running are listed in unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
no | Session ids. |
all |
boolean |
no | Every session, instead of ids. |
Request
POST /v1/sessions/delete#
Delete sessions, running ones included. Stops the session if it runs, then removes it with its browser data. There is no confirmation and no undo.
| Field | Type | Required | Description |
|---|---|---|---|
ids |
string[] |
yes | Session ids. |
Request
POST /v1/sessions/clear#
Delete every session, like Clear all on the Remote page. Stops what runs, then removes every session with its browser data. There is no confirmation and no undo.
No parameters.
Request
Settings#
GET /v1/settings#
Machine settings. The same fields as Machine settings on the Remote page. cacheLimit is bytes: -1 is the default, 0 turns the cache off.
No parameters.
Response
POST /v1/settings/update#
Change machine settings. Only the fields sent change. hideBrowser, allowPopups, allowAdult, allowCoinMn and cacheLimit apply to sessions started afterwards; autoStart applies at the next launch of the app.
| Field | Type | Required | Description |
|---|---|---|---|
note |
string |
no | Machine name, shown on the Remote page. |
hideBrowser |
boolean |
no | Hide the browser windows. |
allowPopups |
boolean |
no | Accept campaigns with popups. |
allowAdult |
boolean |
no | Accept campaigns with adult content. |
allowCoinMn |
boolean |
no | Accept campaigns with crypto mining. |
cacheLimit |
integer |
no | Disk cache limit per session in bytes; -1 default, 0 off. Min -1. |
autoStart |
boolean |
no | Start every session when the app opens. |
Request
Response
App#
POST /v1/app/exit#
Stop every session and quit the app. Sessions close first; the app exits about ten seconds later, and the API goes away with it (the app.exiting event is sent). Under the Linux install script or Docker the supervisor opens the app again.
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string |
no | Shown in the app's log. |
Request
Events (WebSocket)#
Open a WebSocket to /v1/events?key=<key> (an Authorization header works too) to be told about changes instead of polling. Each message is one JSON text frame { "type", "ts", "data" }.
Send the text ping to get a pong. Send text frames only: a binary frame closes the connection.
| Type | Data |
|---|---|
session.status |
the live part of a session, as GET /v1/sessions/{id} returns it without note and proxy: sent when its status, running, client, hits, points or quality change |
session.added |
{ ids }: new sessions, from the API, the app or the Remote page |
session.updated |
{ ids }: sessions whose note or proxy changed |
session.deleted |
{ ids }: removed sessions |
settings.updated |
the machine settings, as GET /v1/settings returns them |
app.exiting |
{}: Viewer is closing or restarting; the API goes away until it is back |
const ws = new WebSocket("ws://127.0.0.1:37288/v1/events?key=YOUR_API_KEY");
ws.onmessage = (e) => console.log(JSON.parse(e.data));
Limits#
| What | Limit |
|---|---|
HTTP |
HTTP/1.1; a body needs a Content-Length header (chunked bodies are not supported) |
| Request body | 4 MB |
| Requests running at once | 64 (more answer busy) |
| Session ids per call | 1000 |
| Sessions per list page | 1000 (limit) |
| Import text | 1 MB |
| WebSocket connections | 32 |
Troubleshooting#
Connection refused: the API is off, Viewer is closed, or the port changed. The address and port are in Settings; on Linux and Docker the install script prints them.
401 badKey: send Authorization: Bearer <key>. Only the WebSocket takes ?key= in the URL.
Use 127.0.0.1, not localhost, in tools on the same machine: the API listens on IPv4 only, and some tools try IPv6 first for localhost.
Empty reply or connection reset: send HTTP/1.1 and give the body a Content-Length header. HTTP/1.0, chunked bodies and bodies over 4 MB are closed without an answer. Libraries set Content-Length for a string or bytes body; a stream or generator body is sent chunked.
403 with the plain text "origin rejected": the request carried an Origin header, as calls from a web page or browser extension do. Call the API from a script or tool.
The API could not start (shown in Settings): the port is in use. Pick another port and update your tools.
409 duplicate: another session already uses that proxy, or the machine's own IP; details.id names it. Change that session instead, or use another proxy.
Making a new key stops every tool that uses the old one until it gets the new one.