Skip to content

REST API ​

About 17 min read

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.

  1. On the API Keys tab, click the add button above the table. The Api Key Definition dialog opens.

  2. Fill in the fields:

    FieldDescriptionDefaultRange
    User NameThe user who owns the key. The key works with this user's roles and process group permissions. Cannot be changed after saving.
    Key nameA name that identifies the key. Two keys of the same user cannot have the same name.At most 100 characters
    PurposeFor the REST API, select SCADA API. Keys with the MCP access purpose are invalid for the REST API. Cannot be changed after saving.MCP accessSCADA API, MCP access
    Max HitsThe maximum number of requests within 10 minutes.3001–30,000,000
  3. 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:

http
GET /api/Monitor/Devices HTTP/1.1
Host: <server>
ApiKey: <api_key>
bash
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:

http
POST /api/User/login HTTP/1.1
Host: <server>
Content-Type: application/json

{
  "UserName": "<username>",
  "Password": "<password>"
}

Successful response:

json
{
  "Payload": {
    "AccessToken": "<access_token>",
    "RefreshToken": "<refresh_token>",
    "RefreshTokenExpiryTime": "2026-10-22T09:30:00+03:00",
    "PasswordChangeRequired": false
  },
  "Success": true,
  "ErrorMessage": null,
  "ErrorCode": 0
}
FieldDescription
AccessTokenThe access token to send in the Authorization header of the following requests. Valid for 240 minutes by default.
RefreshTokenUsed to refresh the access token. Valid for 7 days by default.
RefreshTokenExpiryTimeThe time the refresh token expires.
PasswordChangeRequiredIf 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:

http
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:

http
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.

json
{
  "Payload": {},
  "Success": true,
  "ErrorMessage": null,
  "ErrorCode": 0
}
FieldDescription
PayloadThe result of the request. Its structure depends on the endpoint. Can be empty or null on error.
Successtrue if the operation succeeded.
ErrorMessageError message (in Turkish). null for successful requests.
ErrorCodeError 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:

json
{
  "Success": false,
  "ErrorMessage": "Geçersiz API Anahtarı"
}

HTTP Status Codes ​

StatusErrorMessageMeaning
401Lütfen Geçerli Gir API Anahtarı SağlayınThe request has neither an Authorization nor an ApiKey header.
401API Anahtarı Doğrulanırken Hata OluştuA server error occurred while validating the key; try again later.
403Geçersiz API AnahtarıThe key was not found, was deleted, has the MCP access purpose, or the account that owns it cannot be used.
403Maksimum Sorgulama Limiti AşıldıThe key's Max Hits was exceeded. The response contains the X-RateLimit-RetryAfter header.
403Geçersiz Giriş TokenThe token in the Authorization header is invalid or expired.
403Bu Kullanıcı Bu Müdahaleye Yetkili DeğilThe role of the user or of the key's owner is not sufficient for this endpoint.

Common Error Codes ​

ErrorCodeMeaning
1101Missing or invalid parameter. In archive queries, ErrorMessage describes the missing field.
1203Archive query limit exceeded (number of log definitions, estimated number of cells, concurrent queries, or query timeout).
1208The requested date range exceeds the allowed limit.
2106The refresh token is invalid.
2107The username or password is wrong.
2108The session has expired; sign in again.
2110Too many failed sign-in attempts; wait a while.
3101Tag not found.
3501Log definition not found.
4400Log table not found.
4401Could not connect to the log database.

Live Data ​

Device States ​

http
GET /api/Monitor/Devices

Lists 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.

json
{
  "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
}
FieldDescription
IdUnique ID of the device.
Activetrue if device communication is enabled.
NameDevice name.
ChannelIdID of the channel the device belongs to.
ConnectionTypeA number for the channel's connection type (see the table below).
ConnectionTypeStrThe connection type as text.
ConnectionQualityQuality of the connection to the device: 0 = Bad, 1 = Good, 2 = Uncertain.
DeviceQualityStrThe ConnectionQuality value as text.
TagQualityRead quality of the device's tags; it turns bad if one of the tags cannot be read. Takes the same values as ConnectionQuality.
TagQualityStrThe TagQuality value as text.
ConnectionDescriptionShort description of the connection parameters.
ErrorStringThe last error message in device communication.
ErrorOccurredTimeThe time of the last error.
ParametersText containing the connection parameters.
AlarmStateState 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.
ChangedTimeThe time of the most recently updated tag of the device. If the device has no tags, 0001-01-01T00:00:00 is returned.
IsOutOfDatetrue if the device definition was changed and the device needs to be restarted.
IsConnectedNot used by this endpoint; always false.
TagsNot used by this endpoint; always null. Use the endpoints below for tag values.

ConnectionType values:

ValueTextValueText
1ModbusSerial8MtConnect
2ModbusTcp9BacNet
3ModbusOverEth10Database
4OpcServer11RestClient
5SiemensTcp13RestServer
6Mqtt14EtherNetIp
7OpcUa999Internal

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:

EndpointDescription
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,5Tags 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.

http
GET /api/Monitor/Devices/TagsByName/Kompresor_1
json
{
  "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
}
FieldDescription
IdUnique ID of the tag.
DeviceIdID of the device the tag belongs to.
DeviceNameName of the device the tag belongs to.
NameTag name.
AddressThe tag's address on the device.
FullNameThe full name in the form Channel.Device.Tag. Unique in the system.
ValueThe last value read for the tag. Depending on the tag's data type, it can be a number, a Boolean or a string.
ChangedTimeThe time the value was last updated.
QualityRead quality: 0 = Bad (cannot be read), 1 = Good (healthy), 2 = Uncertain (for example, not read yet).
QualityStrThe Quality value as text.
IsAlarmDefinedtrue if an alarm is defined on the tag.
IsLogDefinedtrue if a log (archive) is defined on the tag.
UnitThe unit defined on the tag.
DescriptionTag description.

Value of a Single Tag ​

EndpointDescription
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 ​

http
GET /api/Tags/ByName/{name}/LogInfos

Pass 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:

FieldDescription
IdID of the log definition. Put it in the Ids parameter of archive queries.
TagIdID of the tag the log definition belongs to.
DescDescription 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:

http
GET /api/History/QueryLimits
text
{
  "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 ​

http
GET /api/History/Tags?Ids=1,2&StartDate=2026-10-15T08:00:00Z&EndDate=2026-10-15T09:00:00Z
POST /api/History/Tags

Returns all records in the given date range without sampling. With POST, the same fields are sent in the JSON body.

ParameterDescription
IdsLog definition IDs. In the query string they can be separated with commas (Ids=1,2) or repeated (Ids=1&Ids=2).
StartDateStart time. Required.
EndDateEnd 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 ​

http
GET /api/History/TagsSampleBy
POST /api/History/TagsSampleBy

Splits 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.

ParameterDescriptionDefaultValues
IdsLog definition IDs. At least one positive ID is required.
StartDateStart time. Required.
EndDateEnd time. Required; cannot be earlier than StartDate.
SampleTimeSampling interval in seconds.18600Greater than 0
ValueTypeThe calculation for each interval.00 = Min, 1 = Max, 2 = Avg (average), 3 = Sum (total), 4 = Count (number of records)
FillTypeHow intervals without records are filled.00 = NULL (left empty), 1 = PREV (previous value), 2 = LINEAR (linear interpolation), 3 = X (filled with FillValue)
FillValueThe integer value used when FillType = 3.0
AlignToCalendarIf 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.truetrue, false
CalculateConsumptionIf 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.falsetrue, false
IANATimeZoneTime 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):

http
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:

json
{
  "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:

json
{
  "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:

FieldDescription
columnsList 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.
datasetRows. 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.
countNumber of rows.
queryNot used; returned empty.

Consumption Calculation ​

http
POST /api/History/TagsSampleByForPivotDiffSum

Returns, 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.

bash
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:

json
{
  "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
}
FieldDescription
LogInfoIdID of the log definition.
DescDescription 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.
ChangedTimeStart time of the interval (UTC).
ValueConsumption in the interval. If there are no records, it is filled according to FillType or null is returned.
TaskCenterTask center of the device the tag belongs to.
DataGroupProcess 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 ​

SymptomCheck
401, Lütfen Geçerli Gir API Anahtarı SağlayınCheck 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 sentThe 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 tokenThe 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 devicesCheck that the user or the key's owner has permission for the related process group.
A query by device name returns an empty listCheck that the device name is written exactly, including case, and check the URL encoding.
An archive query returns 1203Reduce 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 1208Narrow the date range of the raw archive query or use the sampled archive endpoint.
An archive query returns 3501Check that the Ids values are log definition IDs, not tag IDs. For the consumption calculation, check that CalculateConsumption is true.
An archive query returns 4401Check that the log database service is running.
Archive times are off by a few hoursTimes in responses are UTC. State the time zone explicitly in the request (Z or +03:00).
An HTTPS request gives a certificate errorCheck that the server certificate is trusted on the client side.

2WinScada 2.10