REST API
The 2WinScada REST API lets third-party software read the current device and tag states and query archive (log) data. This page covers only the endpoints suitable for external integration, which can also be used with an API key.
The endpoints on this page are read-only. To send data from external systems to 2WinScada, use the REST Server channel; to let 2WinScada poll data from an external system periodically, use the REST Client channel.
Server Address
The API is published on the same address and port as the web interface. All paths on this page are appended to the address of the web interface; for example https://<server>/api/Monitor/Devices.
Replace <server> with the address that users open 2WinScada with in the browser (a DNS name or an IP address). If the web interface is published on a port other than the default, add the port as well (https://<server>:<port>). Use HTTPS so that credentials and API keys are not carried over the network as plain text. Contact your system integrator to change the address or the port.
Authentication
Requests are authorized in one of two ways:
- API key: Recommended for machine-to-machine integration. No sign-in or token refresh is needed.
- User sign-in (token): You sign in with a user defined in the system and send the returned access token with every request.
The two methods are not used together in the same request. If the request has an Authorization header, the ApiKey header is ignored; an invalid or expired token causes the request to be rejected even if a valid API key is present.
With an API Key
API keys are created on the API Keys tab of the User Management page. For details, see the API Keys page.
On the API Keys tab, click the add button above the table. The Api Key Definition dialog opens.
Fill in the fields:
Field Description Default Range User Name The user who owns the key. The key works with this user's roles and process group permissions. Cannot be changed after saving. Key name A name that identifies the key. Two keys of the same user cannot have the same name. At most 100 characters Purpose For the REST API, select SCADA API. Keys with the MCP access purpose are invalid for the REST API. Cannot be changed after saving. MCP access SCADA API, MCP access Max Hits The maximum number of requests within 10 minutes. 300 1–30,000,000 Save. The key is shown only once, in the API key created dialog. Copy it with Copy and store it in a safe place, then click I saved it, close. The key cannot be displayed again later; if it is lost, create a new key.
Keys with the SCADA API purpose start with 2Win-. A user can have more than one key; this way you can give each integration its own key and revoke only that key when needed.
The key is sent in the ApiKey header of every request:
GET /api/Monitor/Devices HTTP/1.1
Host: <server>
ApiKey: <api_key>curl -H "ApiKey: <api_key>" "https://<server>/api/Monitor/Devices"Rules for using keys:
- Keys do not expire. To revoke a key, delete it on the API Keys tab; requests with a deleted key are rejected immediately. Editing can change only Key name and Max Hits.
- If accounts cannot sign in because the account limit in the license is exceeded, the keys of those accounts are rejected as well (see License).
- The Hit Count and Last Hit Time columns show the key's usage. Requests are counted in fixed 10-minute windows; a window starts with the first request after the previous window has ended. When Max Hits is exceeded, requests made with the key are rejected until the window ends; in the new window the counter starts from zero and the client can make requests again. Choose the polling frequency and the Max Hits value accordingly.
- Archive queries made with an API key have narrower limits than a user session (see Query Limits).
With User Sign-in
To work with a token, sign in first:
POST /api/User/login HTTP/1.1
Host: <server>
Content-Type: application/json
{
"UserName": "<username>",
"Password": "<password>"
}Successful response:
{
"Payload": {
"AccessToken": "<access_token>",
"RefreshToken": "<refresh_token>",
"RefreshTokenExpiryTime": "2026-10-22T09:30:00+03:00",
"PasswordChangeRequired": false
},
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}| Field | Description |
|---|---|
AccessToken | The access token to send in the Authorization header of the following requests. Valid for 240 minutes by default. |
RefreshToken | Used to refresh the access token. Valid for 7 days by default. |
RefreshTokenExpiryTime | The time the refresh token expires. |
PasswordChangeRequired | If true, the account's password must be changed. Change the password of the account used for the integration in the interface first. |
If the username or password is wrong, Success is false and ErrorCode is 2107. After many failed attempts, sign-in is temporarily locked (2110).
In the following requests, the access token is sent in the Authorization header with the Bearer prefix:
GET /api/Monitor/Devices HTTP/1.1
Host: <server>
Authorization: Bearer <access_token>Token Refresh
When the access token expires, the endpoints on this page return HTTP 403 with "ErrorMessage": "Geçersiz Giriş Token". In that case, get a new access token with the expired access token and the refresh token:
PUT /api/User/RefreshToken HTTP/1.1
Host: <server>
Content-Type: application/json
{
"AccessToken": "<expired_access_token>",
"RefreshToken": "<refresh_token>"
}The response has the same structure as the sign-in response. Payload.AccessToken holds the new access token; the refresh token does not change.
If the refresh fails, the HTTP status code is still 200 and Success is false. If ErrorCode is 2108, the refresh token has expired; if it is 2106, the refresh token is invalid. In both cases, sign in again.
The server keeps a single refresh token per account. Every sign-in with the same account (including sign-ins in the web interface) creates a new refresh token and invalidates the previous one. For an integration, use a separate account that is not used in the web interface, or an API key.
From 2.11 the web interface keeps the refresh token of its own session in a cookie that browser scripts cannot read. This is a mode that only the web interface turns on; the sign-in and refresh calls on this page do not change, and the refresh token is still returned in the response body. The MCP server is not affected either.
Response Structure
All endpoints return the same JSON envelope. Field names start with a capital letter (PascalCase); field names in the request body are case-insensitive.
{
"Payload": {},
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}| Field | Description |
|---|---|
Payload | The result of the request. Its structure depends on the endpoint. Can be empty or null on error. |
Success | true if the operation succeeded. |
ErrorMessage | Error message (in Turkish). null for successful requests. |
ErrorCode | Error code. 0 for successful requests. |
Business rule errors (missing parameter, log definition not found, query limit and so on) are returned with HTTP 200 and Success: false. The client must check the Success field in every response.
Requests rejected during authentication return status code 401 or 403. These responses contain only the Success and ErrorMessage fields; there is no ErrorCode:
{
"Success": false,
"ErrorMessage": "Geçersiz API Anahtarı"
}HTTP Status Codes
| Status | ErrorMessage | Meaning |
|---|---|---|
401 | Lütfen Geçerli Gir API Anahtarı Sağlayın | The request has neither an Authorization nor an ApiKey header. |
401 | API Anahtarı Doğrulanırken Hata Oluştu | A server error occurred while validating the key; try again later. |
403 | Geçersiz API Anahtarı | The key was not found, was deleted, has the MCP access purpose, or the account that owns it cannot be used. |
403 | Maksimum Sorgulama Limiti Aşıldı | The key's Max Hits was exceeded. The response contains the X-RateLimit-RetryAfter header. |
403 | Geçersiz Giriş Token | The token in the Authorization header is invalid or expired. |
403 | Bu Kullanıcı Bu Müdahaleye Yetkili Değil | The role of the user or of the key's owner is not sufficient for this endpoint. |
Common Error Codes
ErrorCode | Meaning |
|---|---|
1101 | Missing or invalid parameter. In archive queries, ErrorMessage describes the missing field. |
1203 | Archive query limit exceeded (number of log definitions, estimated number of cells, concurrent queries, or query timeout). |
1208 | The requested date range exceeds the allowed limit. |
2106 | The refresh token is invalid. |
2107 | The username or password is wrong. |
2108 | The session has expired; sign in again. |
2110 | Too many failed sign-in attempts; wait a while. |
3101 | Tag not found. |
3501 | Log definition not found. |
4400 | Log table not found. |
4401 | Could not connect to the log database. |
Live Data
Device States
GET /api/Monitor/DevicesLists the devices defined in the system together with their communication and last error states. Users without the Editor role, and API keys owned by such users, see only the devices in the process groups they are authorized for.
{
"Payload": [
{
"Id": 3,
"Active": true,
"Name": "Kompresor_1",
"ChannelId": 1,
"ConnectionType": 2,
"ConnectionTypeStr": "ModbusTcp",
"ConnectionQuality": 1,
"DeviceQualityStr": "Good",
"TagQuality": 1,
"TagQualityStr": "Good",
"ConnectionDescription": "192.168.1.10:502",
"ErrorString": null,
"ErrorOccurredTime": "0001-01-01T00:00:00",
"Parameters": "",
"AlarmState": -1,
"ChangedTime": "2026-10-15T10:42:07.123+03:00",
"IsOutOfDate": false,
"IsConnected": false,
"Tags": null
}
],
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}| Field | Description |
|---|---|
Id | Unique ID of the device. |
Active | true if device communication is enabled. |
Name | Device name. |
ChannelId | ID of the channel the device belongs to. |
ConnectionType | A number for the channel's connection type (see the table below). |
ConnectionTypeStr | The connection type as text. |
ConnectionQuality | Quality of the connection to the device: 0 = Bad, 1 = Good, 2 = Uncertain. |
DeviceQualityStr | The ConnectionQuality value as text. |
TagQuality | Read quality of the device's tags; it turns bad if one of the tags cannot be read. Takes the same values as ConnectionQuality. |
TagQualityStr | The TagQuality value as text. |
ConnectionDescription | Short description of the connection parameters. |
ErrorString | The last error message in device communication. |
ErrorOccurredTime | The time of the last error. |
Parameters | Text containing the connection parameters. |
AlarmState | State of the alarms defined on the device's tags: -1 = no alarm, 0 = alarm cleared, 1 = alarm condition gone, awaiting reset, 2 = alarm acknowledged, 3 = new alarm. |
ChangedTime | The time of the most recently updated tag of the device. If the device has no tags, 0001-01-01T00:00:00 is returned. |
IsOutOfDate | true if the device definition was changed and the device needs to be restarted. |
IsConnected | Not used by this endpoint; always false. |
Tags | Not used by this endpoint; always null. Use the endpoints below for tag values. |
ConnectionType values:
| Value | Text | Value | Text |
|---|---|---|---|
1 | ModbusSerial | 8 | MtConnect |
2 | ModbusTcp | 9 | BacNet |
3 | ModbusOverEth | 10 | Database |
4 | OpcServer | 11 | RestClient |
5 | SiemensTcp | 13 | RestServer |
6 | Mqtt | 14 | EtherNetIp |
7 | OpcUa | 999 | Internal |
Values that are not in the table belong to legacy connection types that this guide does not cover.
Tag Values of a Device
The current values of all tags of a device can be read with three endpoints:
| Endpoint | Description |
|---|---|
GET /api/Monitor/Devices/TagsByName/{deviceName} | By device name. The name is case-sensitive. |
GET /api/Monitor/Devices/TagsById/{deviceId} | By device ID. |
GET /api/Monitor/Devices/Tags/ByDeviceIds?deviceIds=3,5 | Tags of several devices in one list. IDs can be separated with commas or repeated as deviceIds=3&deviceIds=5. A value that is not a number returns HTTP 400. |
If the device is not found, no error is returned; Payload is an empty list.
GET /api/Monitor/Devices/TagsByName/Kompresor_1{
"Payload": [
{
"Id": 41,
"DeviceId": 3,
"DeviceName": "Kompresor_1",
"Name": "Basinc",
"Address": "40001",
"FullName": "Kanal_1.Kompresor_1.Basinc",
"Value": 6.82,
"ChangedTime": "2026-10-15T10:42:07.123+03:00",
"Quality": 1,
"QualityStr": "Good",
"IsAlarmDefined": true,
"IsLogDefined": true,
"Unit": "bar",
"Description": "Hat basıncı"
}
],
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}| Field | Description |
|---|---|
Id | Unique ID of the tag. |
DeviceId | ID of the device the tag belongs to. |
DeviceName | Name of the device the tag belongs to. |
Name | Tag name. |
Address | The tag's address on the device. |
FullName | The full name in the form Channel.Device.Tag. Unique in the system. |
Value | The last value read for the tag. Depending on the tag's data type, it can be a number, a Boolean or a string. |
ChangedTime | The time the value was last updated. |
Quality | Read quality: 0 = Bad (cannot be read), 1 = Good (healthy), 2 = Uncertain (for example, not read yet). |
QualityStr | The Quality value as text. |
IsAlarmDefined | true if an alarm is defined on the tag. |
IsLogDefined | true if a log (archive) is defined on the tag. |
Unit | The unit defined on the tag. |
Description | Tag description. |
Value of a Single Tag
| Endpoint | Description |
|---|---|
GET /api/Monitor/Tag/{tagFullName} | By full name (Channel.Device.Tag). |
GET /api/Monitor/TagById/{tagId} | By tag ID. |
Payload has the same structure as the tag object above. If the tag is not found, Success is true and Payload is null. Because the full name is sent in the URL, write spaces and special characters URL-encoded (e.g. %20 for a space).
Archive Data
The archive endpoints query the recorded data of tags that have a log (archive) definition. Queries use the ID of the log definition (Ids), not the tag ID. For log definitions, see the Log (Archive) Definitions page.
Finding the Log Definition ID
GET /api/Tags/ByName/{name}/LogInfosPass the tag's full name (Channel.Device.Tag) as {name}. The tag name alone is also accepted, but if there is more than one tag with the same name, only the log definitions of the first one found are returned. If the tag is not found, ErrorCode is 3101.
Payload is the list of the tag's log definitions. The fields to use in queries:
| Field | Description |
|---|---|
Id | ID of the log definition. Put it in the Ids parameter of archive queries. |
TagId | ID of the tag the log definition belongs to. |
Desc | Description of the log definition. |
Query Limits
Archive queries are limited to protect the server: the query duration, the number of log definitions in one query, the number of returned rows and the date range of a raw archive query are limited. Queries made with an API key have narrower limits.
With an API key, send archive queries one after another, not at the same time; a query sent concurrently can be rejected with code 1203.
The limits in effect on the server can be read with this endpoint:
GET /api/History/QueryLimits{
"Payload": {
"Profile": "ApiKey",
"QueryTimeoutSeconds": <seconds>,
"MaxLogInfoCount": <number>,
"MaxEstimatedCells": <number>,
"MaxRows": <number>,
"MaxRawTagRangeSeconds": <seconds>,
"MaxEventRangeSeconds": <seconds>,
"MaxSnapshotItems": <number>
},
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}Profile is ApiKey for requests made with an API key and Interactive for a user session.
Date Format
StartDate and EndDate are given in ISO 8601 format, for example 2026-10-15T08:00:00Z or 2026-10-15T08:00:00+03:00. In the query string, the yyyy-MM-dd HH:mm and yyyy-MM-dd HH:mm:ss formats are also accepted. If no time zone is given, the server's local time is assumed. Encode the + character as %2B in the query string. Archive times in responses are returned in UTC (Z).
Raw Archive Records
GET /api/History/Tags?Ids=1,2&StartDate=2026-10-15T08:00:00Z&EndDate=2026-10-15T09:00:00Z
POST /api/History/TagsReturns all records in the given date range without sampling. With POST, the same fields are sent in the JSON body.
| Parameter | Description |
|---|---|
Ids | Log definition IDs. In the query string they can be separated with commas (Ids=1,2) or repeated (Ids=1&Ids=2). |
StartDate | Start time. Required. |
EndDate | End time. Required; the date range cannot exceed the raw archive limit. |
The response has the same table structure as the sampled archive response.
Sampled Archive
GET /api/History/TagsSampleBy
POST /api/History/TagsSampleBySplits the records into intervals defined by SampleTime and performs the calculation selected with ValueType for each interval. If the sample time is set equal to the record interval, the records are returned close to their raw form.
| Parameter | Description | Default | Values |
|---|---|---|---|
Ids | Log definition IDs. At least one positive ID is required. | ||
StartDate | Start time. Required. | ||
EndDate | End time. Required; cannot be earlier than StartDate. | ||
SampleTime | Sampling interval in seconds. | 18600 | Greater than 0 |
ValueType | The calculation for each interval. | 0 | 0 = Min, 1 = Max, 2 = Avg (average), 3 = Sum (total), 4 = Count (number of records) |
FillType | How intervals without records are filled. | 0 | 0 = NULL (left empty), 1 = PREV (previous value), 2 = LINEAR (linear interpolation), 3 = X (filled with FillValue) |
FillValue | The integer value used when FillType = 3. | 0 | |
AlignToCalendar | If true, intervals are aligned to the calendar (starts of hours, days, months in the IANATimeZone time zone); if false, intervals start at the time of the first record. | true | true, false |
CalculateConsumption | If true, instead of ValueType the consumption of each interval is calculated according to the consumption settings of the log definition: instant values are summed; for cumulative counters, the increases between consecutive values are summed starting from the last value before the interval (a counter reset counts as a step of 0); the result is scaled with the factor and divisor of the log definition. | false | true, false |
IANATimeZone | Time zone for calendar alignment, for example Europe/Istanbul. If left empty, the server's time zone is used. |
In the query string, ValueType and FillType can be written as a number or as a name (ValueType=Max); in the JSON body they must be sent as numbers.
Example query (maximum value in half-hour intervals):
GET /api/History/TagsSampleBy?Ids=1&ValueType=1&FillType=0&FillValue=0&SampleTime=1800&AlignToCalendar=false&CalculateConsumption=false&StartDate=2026-10-15T08:00:00Z&EndDate=2026-10-15T09:00:00Z HTTP/1.1
Host: <server>
ApiKey: <api_key>The body when the same query is sent with POST:
{
"Ids": [1],
"ValueType": 1,
"FillType": 0,
"FillValue": 0,
"SampleTime": 1800,
"AlignToCalendar": false,
"CalculateConsumption": false,
"StartDate": "2026-10-15T08:00:00Z",
"EndDate": "2026-10-15T09:00:00Z"
}Response:
{
"Payload": {
"query": "",
"columns": [
{ "name": "ChangedTime", "type": "timestamp" },
{ "name": "1", "type": "DOUBLE" }
],
"dataset": [
["2026-10-15T08:00:37Z", 1194.13919413919],
["2026-10-15T08:30:37Z", 1175.82417582418]
],
"count": 2
},
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}Field names inside Payload are written in lower case:
| Field | Description |
|---|---|
columns | List of columns. The first column is ChangedTime; every following column is a log definition, and the column name is the ID of the log definition. |
dataset | Rows. The first element of each row is the UTC time; the following elements are the values of the log definitions in the order of columns. For a log definition without a value at that time, null is returned. |
count | Number of rows. |
query | Not used; returned empty. |
Consumption Calculation
POST /api/History/TagsSampleByForPivotDiffSumReturns, row by row, the consumption values of log definitions in the given intervals, together with task center and process group information. The body takes the same fields as the sampled archive; on this endpoint CalculateConsumption must be sent as true, otherwise code 3501 is returned.
For cumulative counter log definitions, the differences between consecutive records are summed; drops such as a counter reset count as 0. For other log definitions, the values are summed. The result is scaled with the factor and divisor of the log definition. To get the total consumption of the date range as a single value, set SampleTime to the length of the range in seconds.
curl -X POST "https://<server>/api/History/TagsSampleByForPivotDiffSum" \
-H "ApiKey: <api_key>" \
-H "Content-Type: application/json" \
-d '{
"Ids": [1, 2],
"ValueType": 1,
"FillType": 0,
"FillValue": 0,
"SampleTime": 3600,
"AlignToCalendar": false,
"CalculateConsumption": true,
"StartDate": "2026-10-15T08:00:00Z",
"EndDate": "2026-10-15T09:00:00Z"
}'Response:
{
"Payload": [
{
"LogInfoId": 1,
"Desc": "Hat_1_Buhar [ton]",
"ChangedTime": "2026-10-15T08:00:37Z",
"Value": 0.22014652014652,
"TaskCenter": "Uretim",
"DataGroup": "Buhar_Tuketimi"
},
{
"LogInfoId": 2,
"Desc": "Hat_2_Buhar [ton]",
"ChangedTime": "2026-10-15T08:00:37Z",
"Value": 0.7927872860635696,
"TaskCenter": "Uretim",
"DataGroup": "Buhar_Tuketimi"
}
],
"Success": true,
"ErrorMessage": null,
"ErrorCode": 0
}| Field | Description |
|---|---|
LogInfoId | ID of the log definition. |
Desc | Description of the log definition and its unit in square brackets. If a total value unit is entered in the log definition, that unit is used; otherwise the tag's unit. |
ChangedTime | Start time of the interval (UTC). |
Value | Consumption in the interval. If there are no records, it is filled according to FillType or null is returned. |
TaskCenter | Task center of the device the tag belongs to. |
DataGroup | Process group of the device's task center; if there is none, the tag group the tag belongs to. |
Swagger
If API documentation is enabled on the server, all endpoints and their schemas can be explored and tried in the Swagger interface. API documentation is off by default; contact your system integrator to enable it and for its address. In Swagger, you can send requests after entering an ApiKey or Authorization value with Authorize.
Troubleshooting
| Symptom | Check |
|---|---|
401, Lütfen Geçerli Gir API Anahtarı Sağlayın | Check that the request has an ApiKey or Authorization header and that the header name is spelled correctly. |
403, Geçersiz API Anahtarı | Check that the key was copied completely, was not deleted, and that its Purpose is SCADA API. MCP access keys do not work with the REST API. |
Geçersiz Giriş Token is returned although an API key is sent | The request also sends an Authorization header. Remove this header. |
403, Maksimum Sorgulama Limiti Aşıldı | Try again after the 10-minute window has ended. Reduce the polling frequency or increase the Max Hits value. |
403, Geçersiz Giriş Token after a few hours when working with a token | The access token has expired. Do a token refresh; if the refresh returns 2106 or 2108, sign in again. |
/api/Monitor/Devices does not return some devices | Check that the user or the key's owner has permission for the related process group. |
| A query by device name returns an empty list | Check that the device name is written exactly, including case, and check the URL encoding. |
An archive query returns 1203 | Reduce the number of log definitions, the date range or the sampling frequency; with an API key, send archive queries one after another, not at the same time. Check the limits with GET /api/History/QueryLimits. |
An archive query returns 1208 | Narrow the date range of the raw archive query or use the sampled archive endpoint. |
An archive query returns 3501 | Check that the Ids values are log definition IDs, not tag IDs. For the consumption calculation, check that CalculateConsumption is true. |
An archive query returns 4401 | Check that the log database service is running. |
| Archive times are off by a few hours | Times in responses are UTC. State the time zone explicitly in the request (Z or +03:00). |
| An HTTPS request gives a certificate error | Check that the server certificate is trusted on the client side. |