This document provides a comprehensive reference for the WINK Streaming Command API, covering WINK Forge, WINK Archive, and WINK Media Router appliances. The API enables programmatic control of video streaming operations, user management, and system administration.
The WINK API uses XML-based messaging over HTTPS, providing:
All API calls require authentication using the following XML structure:
<wink_api user='username' pass='password' key='shared_key'>
<req id='unique_id' command='command_name'>payload</req>
</wink_api>
| Parameter | Required | Description |
|---|---|---|
| user | Yes | API username (must have API access level) |
| pass | Yes | User password |
| key | Optional | Shared key for enhanced security |
All API endpoints require SSL/HTTPS connections. Configure SSL certificates through:
--insecure flag with cURL to bypass certificate validationcurl -X POST https://10.130.90.188/api/ \
--insecure \
--data-urlencode "<wink_api user='apiuser' pass='apipass'>
<req id='1' command='version'></req>
</wink_api>" \
-H 'Content-Type: application/xml'
WINK Forge includes a built-in API testing interface:
Access via: Tools → API Tester
Built-in API testing interface
| Command | version |
|---|---|
| Description | Returns the API version number |
| Payload | None |
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='version'></req>
</wink_api>
|
| Response |
<wink_api>
<resp id="1" command="version" status="ok">1.4</resp>
</wink_api>
|
| Return Value | Numeric API version (e.g., "1.4") |
| Command | start | stop | restart |
|---|---|
| Description | Controls transcoding operations for input streams |
| Payload | GUID of the input stream or 'all' for all streams |
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='restart'>WF05-2657-1C04-7F1B-5EC0</req>
</wink_api>
|
| Response |
<wink_api>
<resp id="1" command="restart" status="ok">WF05-2657-1C04-7F1B-5EC0</resp>
</wink_api>
|
| Return Value | GUID of affected input or error message |
| Command | startip | stopip |
|---|---|
| Description | Controls transcoding based on IP:Port combination |
| Payload | IP address and port (format: IP:PORT) |
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='stopip'>10.130.90.68:554</req>
</wink_api>
|
| Response |
<wink_api>
<resp id="1" command="stopip" status="ok">WF05-2657-1C04-7F1B-5EC0</resp>
</wink_api>
|
| Return Value | GUID of the affected input |
| Command | getguid |
|---|---|
| Description | Returns GUID for a specific IP:Port combination |
| Payload | IP and Port (e.g., 10.130.90.199:554) |
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='getguid'>10.130.90.199:8382</req>
</wink_api>
|
| Response |
<wink_api>
<resp id="1" command="getguid" status="ok">WF05-2657-1C04-7F1B-5EC0</resp>
</wink_api>
|
| Command | guidstatus |
|---|---|
| Description | Returns the current status of a stream |
| Payload | GUID string |
| Response Values |
|
| Command | blockguid |
|---|---|
| Description | Blocks video output from viewers |
| Payload Options |
|
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='blockguid'>WF05-2657-1C04-7F1B-5EC0</req>
</wink_api>
|
| Command | unblockguid |
|---|---|
| Description | Restores video output to viewers |
| Payload | GUID string |
| Command | blockip |
|---|---|
| Description | Blocks video based on IP:Port without knowing GUID |
| Payload | IP:Port combination (e.g., 10.130.90.199:554) |
| Response | Returns the GUID of the blocked stream |
| Command | unblockip |
|---|---|
| Description | Restores video based on IP:Port |
| Payload | IP:Port combination |
| Command | input_source | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Description | Creates or updates video input configuration | ||||||||||||||||||||||||||||||
| Required (New) |
|
||||||||||||||||||||||||||||||
| Required (Update) | guid - Existing stream GUID | ||||||||||||||||||||||||||||||
| Optional Parameters |
|
||||||||||||||||||||||||||||||
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='input_source'>
<input title='Test Camera'
desc='Testing stream'
type='RTSP'
path='rtsp://192.168.1.100:554/stream1'
fps='30'
width='1920'
height='1080'
enabled='1' />
</req>
</wink_api>
|
| Command | input_delete |
|---|---|
| Description | Removes video input and associated outputs |
| Payload | GUID string |
| Command | createuser |
|---|---|
| Description | Creates a local user account |
| Parameters |
|
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='createuser'>
<name>Billy Armstrong</name>
<username>billy</username>
<password>SecurePass123!</password>
<access>2</access>
</req>
</wink_api>
|
| Command | deleteuser |
|---|---|
| Description | Removes a user account |
| Payload | XML with username |
| Command | changepassword |
|---|---|
| Description | Updates user password |
| Parameters |
|
| Command | log |
|---|---|
| Description | Adds entry to system event log |
| Payload | Log message string |
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='log'>API integration test successful</req>
</wink_api>
|
| Command | flushlogs |
|---|---|
| Description | Clears the event log |
| Payload | None |
| Command | reboot |
|---|---|
| Description | Initiates system reboot |
| Payload | None |
| Response | Error message or immediate reboot |
Firmware Requirements: Version 1.1 or greater
TITLE_[YYYY-MM-DD_HH.MM.SS]_GUID.mp4EastSideSony_[2015-04-03_13.06.18]_WMA5-6E0F-1C04-86F2-E5C4.mp4
Same as WINK Forge version command - returns API version number.
| Commands | archiver_start | archiver_stop |
|---|---|
| Description | Controls video archiving operations |
| Payload | GUID or blank (blank = all archivers) |
| Behavior | If already recording, creates overlapping instance to ensure no content loss |
| Example |
<wink_api user='apiuser' pass='apipass'>
<req id='1' command='archiver_start'>WMA5-6E0F-1C04-86F2-E5C4</req>
</wink_api>
|
| Command | archiver_status |
|---|---|
| Description | Returns archiver configuration and status |
| Payload | GUID or blank (blank = all archivers) |
| Response |
<wink_api>
<resp id="11" command="archiver_status" status="ok">
<archiver guid="WMA5-6E0F-1C04-86F2-E5C4"
title="TV2"
desc="Conference Room Feed"
type="RTMP"
path="rtmp://10.130.90.10/public/stream_high"
segmenttime="5"
meta1="Building A"
meta2="Floor 2"
meta3="Room 201"
meta4="PTZ Camera"
enabled="1" />
</resp>
</wink_api>
|
| Command | archiver_source |
|---|---|
| Description | Create or modify archiver configuration |
| Parameters |
|
| Command | archiver_delete |
|---|---|
| Description | Stop and remove archiver |
| Payload | GUID |
The OTP system provides secure, time-limited access to video streams. Key features:
OTP authentication flow
The OTP actions live on two different paths. Token creation and token management are handled separately:
| Action | Endpoint |
|---|---|
| create | http://router.example.com/otp/api/ |
| extend | query | destroy | http://router.example.com/api/v1/otp/ |
Both endpoints accept form-encoded HTTP POST data and require apiuser and apipass on every request.
The token is appended to the playlist request as an otp query parameter, alongside an expires timestamp:
http://router.example.com/200/public/hls/<stream>.m3u8?otp=a1b2c3d4e5f6&expires=1691234567
Only the .m3u8 request needs the token. Segment requests inherit the authenticated session, so there is no need to rewrite individual .ts URLs.
Tokens are cluster-wide across a deployment. A token created on one Media Router is valid on every Media Router in that deployment, so a five-router deployment still requires only one token per viewer session. For this to work, the API credentials must be defined identically on each Media Router.
| Command | create | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Method | HTTP POST | |||||||||||||||||||||
| Endpoint | /otp/api/ |
|||||||||||||||||||||
| Parameters |
|
|||||||||||||||||||||
| Example |
curl -d "apiuser=apiuser&apipass=apipass&action=create&duration=15&hash_type=numeric&hash_length=20" \
-X POST http://router.example.com/otp/api/
|
|||||||||||||||||||||
| Response | 24814928371014572819 | |||||||||||||||||||||
| Notes | The create call returns the raw token string on its own, with no wrapper or formatting. Errors are returned as 0: followed by a description (for example 0:Invalid Login). |
| Commands | destroy | extend |
|---|---|
| Description | Manage existing OTP tokens |
| Endpoint | /api/v1/otp/ |
| Parameters |
|
| Example |
curl -d "apiuser=apiuser&apipass=apipass&action=extend&token=24814928371014572819&duration=900" \
-X POST http://router.example.com/api/v1/otp/
|
| Response | Success | Failed |
| Command | query |
|---|---|
| Description | Returns token expiration time |
| Endpoint | /api/v1/otp/ |
| Parameters |
|
| Example |
curl -d "apiuser=apiuser&apipass=apipass&action=query&token=24814928371014572819" \
-X POST http://router.example.com/api/v1/otp/
|
| Response Format | YYYY-MM-DD HH:MM:SS |
The questions below come up regularly during web portal and partner integrations.
As an otp query parameter on the playlist request, alongside an expires timestamp:
http://router.example.com/200/public/hls/<stream>.m3u8?otp=a1b2c3d4e5f6&expires=1691234567
Only the .m3u8 request needs it. Segment requests inherit the authenticated session, so individual .ts URLs do not need to be rewritten.
No. Issue a token per viewer session. Sharing a single token across every visitor removes the ability to expire, extend, or revoke access for one viewer, and it makes the access log useless for audit purposes.
One. Tokens are cluster-wide, so a token created for a session spans every Media Router in the deployment. Define the API credentials the same way on each Media Router.
Both paths are in use, and the actions live on different ones:
http://router.example.com/otp/api/http://router.example.com/api/v1/otp/Create parameters are action=create, duration=<minutes>, hash_type=<alpha|numeric|alphanumeric>, and hash_length=<max 128, default 32>, plus apiuser and apipass. The create call returns the raw token string on its own. action=query returns the expiration as YYYY-MM-DD HH:MM:SS.
duration is in minutes and accepts 1 through 900. With per-session tokens, start short - on the order of the length of a typical visit - and call extend for sessions that are still watching, rather than issuing long-lived tokens up front.
On expiry_url versus expiry_message: for a public website, neither should be the primary mechanism. Have the page request a fresh token when one lapses; that is a cleaner experience than a redirect mid-stream. Setting expiry_message is worth doing as a fallback, so anything that does slip through shows a sensible message in the player instead of a bare failure.
The limit is high enough that per-session token creation is not a concern - hundreds of requests per second are permitted. Contact support if a deployment is expected to exceed that.
Yes. Stream URLs are unchanged by the upgrade; the paths documented for a deployment stay the same.
The cURL calls above show the raw requests. The examples below show the same calls in context, issuing and maintaining one token per viewer session.
apiuser and apipass must never be exposed to a browser. Tokens are created by your backend, and only the resulting token reaches the client.
Creates a token on the visitor's first request, reuses it for the rest of the session, and extends it when it is close to lapsing:
<?php
session_start();
// Create and manage live on different paths
$otp_create = 'https://router.example.com/otp/api/';
$otp_manage = 'https://router.example.com/api/v1/otp/';
$api_user = 'apiuser';
$api_pass = 'apipass';
$playlist = 'https://router.example.com/200/public/hls/WF05-333F-4D5C-E636-2937.m3u8';
$duration = 15; // minutes, 1-900
function otp_post($url, $fields) {
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($fields),
),
);
$result = @file_get_contents($url, false, stream_context_create($options));
return ($result === false) ? '' : trim($result);
}
// Still watching and the token is nearly up - extend rather than issue a second one
if (!empty($_SESSION['otp_token']) && ($_SESSION['otp_expires'] - time()) < 60) {
$resp = otp_post($otp_manage, array(
'apiuser' => $api_user,
'apipass' => $api_pass,
'action' => 'extend',
'token' => $_SESSION['otp_token'],
'duration' => $duration
));
if ($resp === 'Success') {
$_SESSION['otp_expires'] = time() + ($duration * 60);
} else {
unset($_SESSION['otp_token']); // extend failed - fall through and create
}
}
// No token yet (or the extend failed) - create one for this session
if (empty($_SESSION['otp_token'])) {
$token = otp_post($otp_create, array(
'apiuser' => $api_user,
'apipass' => $api_pass,
'action' => 'create',
'duration' => $duration,
'hash_type' => 'alphanumeric',
'hash_length' => 32
));
// Errors are returned as "0:" followed by a description
if ($token === '' || substr($token, 0, 2) === '0:') {
die('OTP API error: ' . htmlspecialchars($token));
}
$_SESSION['otp_token'] = $token;
$_SESSION['otp_expires'] = time() + ($duration * 60);
}
// Only the .m3u8 carries the token - segment requests inherit the session
$video_url = $playlist
. '?otp=' . urlencode($_SESSION['otp_token'])
. '&expires=' . $_SESSION['otp_expires'];
// Serving the same values as JSON is all the JavaScript example below needs:
// header('Content-Type: application/json');
// echo json_encode(array('token' => $_SESSION['otp_token'], 'expires' => $_SESSION['otp_expires']));
?>
The page asks its own backend for the session's token, builds the playlist URL, and requests a fresh token shortly before the current one lapses:
let session = null; // { token: "a1b2c3d4e5f6", expires: 1691234567 }
async function getSessionToken() {
const res = await fetch('/otp-token.php', { credentials: 'same-origin' });
if (!res.ok) {
throw new Error('Token request failed: ' + res.status);
}
return res.json();
}
function playlistUrl(playlist) {
return playlist
+ '?otp=' + encodeURIComponent(session.token)
+ '&expires=' + session.expires;
}
async function startStream(playlist) {
session = await getSessionToken();
const video = document.getElementById('player');
const url = playlistUrl(playlist);
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url; // Safari and other native HLS players
} else {
hls.loadSource(url); // hls.js or the player of your choice
}
scheduleRefresh(playlist);
}
// Refresh before expiry rather than letting playback fail. This is cleaner than
// redirecting mid-stream with expiry_url; keep expiry_message set as a fallback.
function scheduleRefresh(playlist) {
const msRemaining = (session.expires * 1000) - Date.now() - 60000; // 60s headroom
setTimeout(async function () {
session = await getSessionToken(); // backend extends, or creates a new token
const video = document.getElementById('player');
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = playlistUrl(playlist);
} else {
hls.loadSource(playlistUrl(playlist));
}
scheduleRefresh(playlist);
}, Math.max(msRemaining, 5000));
}
startStream('https://router.example.com/200/public/hls/WF05-333F-4D5C-E636-2937.m3u8');
A small client wrapping the three actions, suitable for an ASP.NET application holding one token per user session:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
public class WinkOtpClient
{
private static readonly HttpClient Http = new HttpClient();
private readonly string _createUrl; // https://router.example.com/otp/api/
private readonly string _manageUrl; // https://router.example.com/api/v1/otp/
private readonly string _user;
private readonly string _pass;
public WinkOtpClient(string createUrl, string manageUrl, string user, string pass)
{
_createUrl = createUrl;
_manageUrl = manageUrl;
_user = user;
_pass = pass;
}
private async Task<string> PostAsync(string url, Dictionary<string, string> fields)
{
fields["apiuser"] = _user;
fields["apipass"] = _pass;
using (var content = new FormUrlEncodedContent(fields))
{
var response = await Http.PostAsync(url, content);
response.EnsureSuccessStatusCode();
return (await response.Content.ReadAsStringAsync()).Trim();
}
}
// Returns the raw token string. Errors arrive as "0:Invalid Login"
public async Task<string> CreateAsync(int durationMinutes = 15)
{
var token = await PostAsync(_createUrl, new Dictionary<string, string>
{
{ "action", "create" },
{ "duration", durationMinutes.ToString() }, // 1-900
{ "hash_type", "alphanumeric" },
{ "hash_length", "32" }
});
if (token.StartsWith("0:"))
{
throw new InvalidOperationException("OTP API error: " + token);
}
return token;
}
public async Task<bool> ExtendAsync(string token, int durationMinutes = 15)
{
var result = await PostAsync(_manageUrl, new Dictionary<string, string>
{
{ "action", "extend" },
{ "token", token },
{ "duration", durationMinutes.ToString() }
});
return result == "Success";
}
public async Task<DateTime> QueryAsync(string token)
{
var expires = await PostAsync(_manageUrl, new Dictionary<string, string>
{
{ "action", "query" },
{ "token", token }
});
return DateTime.ParseExact(expires, "yyyy-MM-dd HH:mm:ss",
CultureInfo.InvariantCulture);
}
public async Task<bool> DestroyAsync(string token)
{
var result = await PostAsync(_manageUrl, new Dictionary<string, string>
{
{ "action", "destroy" },
{ "token", token }
});
return result == "Success";
}
// Only the .m3u8 carries the token - segments inherit the session
public static string BuildPlaylistUrl(string playlist, string token, DateTimeOffset expires)
{
return string.Format("{0}?otp={1}&expires={2}",
playlist,
Uri.EscapeDataString(token),
expires.ToUnixTimeSeconds());
}
}
Usage, holding one token per session:
var otp = new WinkOtpClient("https://router.example.com/otp/api/",
"https://router.example.com/api/v1/otp/",
"apiuser", "apipass");
var token = await otp.CreateAsync(15);
var expires = new DateTimeOffset(await otp.QueryAsync(token), TimeSpan.Zero);
var videoUrl = WinkOtpClient.BuildPlaylistUrl(
"https://router.example.com/200/public/hls/WF05-333F-4D5C-E636-2937.m3u8",
token, expires);
// Later, while the session is still watching:
await otp.ExtendAsync(token, 15);
// When the session ends:
await otp.DestroyAsync(token);
Complete JavaScript function for API communication:
function exec_api_command() {
// Update UI
document.getElementById("resp_payload").value = "Sending API Command";
// Get connection details
var api_host = document.getElementById("req_ipaddr").value;
var payload = document.getElementById("req_payload").value;
// Create request
var xhttp = new XMLHttpRequest();
// Handle response
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
document.getElementById("resp_payload").value = xhttp.responseText.trim();
}
};
// Send request
xhttp.open("POST", "https://" + api_host.trim() + "/api/", true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send(payload);
}
// Example usage
var xmlPayload = `<wink_api user='apiuser' pass='apipass'>
<req id='1' command='guidstatus'>WF05-1EDA-1C04-6E33-ABF9</req>
</wink_api>`;
// Call function with payload
document.getElementById("req_payload").value = xmlPayload;
exec_api_command();
import requests
import xml.etree.ElementTree as ET
class WinkAPI:
def __init__(self, host, username, password, verify_ssl=True):
self.host = host
self.username = username
self.password = password
self.verify_ssl = verify_ssl
self.api_url = f"https://{host}/api/"
def execute_command(self, command, payload="", req_id="1"):
# Build XML request
xml_request = f"""<wink_api user='{self.username}' pass='{self.password}'>
<req id='{req_id}' command='{command}'>{payload}</req>
</wink_api>"""
# Send request
response = requests.post(
self.api_url,
data=xml_request,
headers={'Content-Type': 'application/xml'},
verify=self.verify_ssl
)
# Parse response
root = ET.fromstring(response.text)
resp = root.find('resp')
return {
'status': resp.get('status'),
'command': resp.get('command'),
'data': resp.text
}
# Example usage
api = WinkAPI('10.130.90.188', 'apiuser', 'apipass', verify_ssl=False)
# Get version
version = api.execute_command('version')
print(f"API Version: {version['data']}")
# Check stream status
status = api.execute_command('guidstatus', 'WF05-1EDA-1C04-6E33-ABF9')
print(f"Stream Status: {status['data']}")
# Get API version
curl -X POST https://forge.example.com/api/ \
--insecure \
--data-urlencode "<wink_api user='apiuser' pass='apipass'><req id='1' command='version'></req></wink_api>" \
-H 'Content-Type: application/xml'
# Start all streams
curl -X POST https://forge.example.com/api/ \
--insecure \
--data-urlencode "<wink_api user='apiuser' pass='apipass'><req id='1' command='start'>all</req></wink_api>" \
-H 'Content-Type: application/xml'
# Create OTP token
curl -d "apiuser=apiuser&apipass=apipass&action=create&duration=60" \
-X POST https://router.example.com/otp/api/
# Create new input with full configuration
curl -X POST https://forge.example.com/api/ \
--insecure \
--data-urlencode "<wink_api user='apiuser' pass='apipass'>
<req id='1' command='input_source'>
<input title='Conference Room'
desc='Main conference room HD camera'
type='RTSP'
path='rtsp://admin:password@192.168.1.100:554/stream1'
fps='30'
width='1920'
height='1080'
ptz_avail='1'
usrlat='41.8781'
usrlong='-87.6298'
enabled='1' />
</req>
</wink_api>" \
-H 'Content-Type: application/xml'
extend for sessions still watching, rather than issuing long-lived tokens up front| Email Support | support@wink.co |
|---|---|
| Phone Support | +1-312-281-5433 |
| Corporate Website | https://www.wink.co |
When contacting support, please provide:
© 2025 WINK Streaming, Inc. All rights reserved.
Document Version: 1.4.9.1 - Updated August 2026
Original Issue: August 2022