API basics
This section is for the developer who connects an accounting or ERP system to the WMS. It describes how your code signs in, which headers every request carries, and how to read the answers. The pages that follow cover each exchange operation.
The WMS is a cloud service: every company works with the same service at https://app.wms.com.ua, and all requests in this section go to that address. Before you start, an administrator of your company sets up the WMS side and gives you three things — see the hand-off checklist:
- the company code (on the Client sign-in screen it is the Tenant field);
- the technical token — a long-lived secret of the technical user;
- the external system code — the code under which your system is registered.
API description
The service publishes a machine-readable description of its whole API. The integration endpoints are listed under the tags that start with Integrations. The description is published on the test environment:
- Swagger UI — browse and try the API
- ReDoc — the same description as one long page
- OpenAPI (JSON) — for client generators and Postman
Signing in
Your code does not send the technical token with every request. It exchanges the token for a short-lived access token and uses that instead:
POST /api/v1/auth/technical/jwt
Authorization: Bearer <technical token>
WMS-Tenant-Code: <company code>
The request has no body. The answer:
{
"accessToken": "eyJhbGciOi…",
"expiresAt": "2026-10-27T10:15:00.123Z"
}
- An access token lives for about 10 minutes. Keep it and reuse it until shortly before
expiresAt, then sign in again. - A technical user can hold only a few access tokens at the same time. Signing in on every request pushes out the tokens of your other running processes, so cache the token.
- If a request gets 401, the access token has expired or was revoked: sign in again and repeat the request once. If the sign-in itself gets 401, the technical token is wrong, expired or was replaced — ask the administrator for a new one (Issuing a token).
Request headers
| Header | When | What it carries |
|---|---|---|
Authorization: Bearer <access token> | every request | the access token from the sign-in |
WMS-Tenant-Code | every request | the company code |
Content-Type: application/json | requests with a body | all bodies are JSON in UTF-8 |
WMS-Idempotency-Key | import requests | a unique key of this request, so that repeating it is safe; see Import |
Accept-Language | optional | the language of error messages, for example uk or en |
WMS-Correlation-Id | optional | your own request id; quote it when you report a problem |
The client used in the examples
Every example in this section builds on the small client below. It signs in, caches the access token, adds the headers and repeats a request once after a 401. The JavaScript and Python versions read the connection settings from environment variables (WMS_BASE_URL, WMS_TENANT, WMS_TOKEN, WMS_EXTERNAL_SYSTEM); the 1C version keeps them in a structure created by НовоеПодключение. Never put the technical token into source code.
- JavaScript
- Python
- 1C
// WMS integration client: exchanges the technical token for a short-lived
// access token (JWT) and sends authorized requests. Node.js 18+, no dependencies.
const config = {
baseUrl: process.env.WMS_BASE_URL, // e.g. https://app.wms.com.ua
tenant: process.env.WMS_TENANT, // company code
token: process.env.WMS_TOKEN, // technical token issued in the WMS Client
externalSystem: process.env.WMS_EXTERNAL_SYSTEM, // external system code
};
let jwt = null; // {accessToken, renewAt}
async function getAccessToken() {
if (jwt && Date.now() < jwt.renewAt) return jwt.accessToken;
const res = await fetch(`${config.baseUrl}/api/v1/auth/technical/jwt`, {
method: 'POST',
headers: {Authorization: `Bearer ${config.token}`, 'WMS-Tenant-Code': config.tenant},
});
if (!res.ok) throw new Error(`WMS sign-in failed: HTTP ${res.status} ${await res.text()}`);
const {accessToken, expiresAt} = await res.json();
// Renew 30 seconds before the token expires.
jwt = {accessToken, renewAt: Date.parse(expiresAt) - 30_000};
return accessToken;
}
class WmsError extends Error {
constructor(status, body) {
const details = body?.errors?.map((e) => `${e.path || '(request)'}: ${e.message}`).join('; ');
super(`WMS HTTP ${status}: ${body?.message ?? body}${details ? ` — ${details}` : ''}`);
this.status = status;
this.body = body;
}
}
async function wmsRequest(method, path, {query = {}, body, idempotencyKey} = {}) {
const url = new URL(config.baseUrl.replace(/\/$/, '') + path);
for (const [name, value] of Object.entries(query)) {
// Arrays become repeated parameters: entityTypes=A&entityTypes=B
for (const item of [].concat(value ?? [])) url.searchParams.append(name, String(item));
}
const send = async () => {
const headers = {
Authorization: `Bearer ${await getAccessToken()}`,
'WMS-Tenant-Code': config.tenant,
Accept: 'application/json',
};
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (idempotencyKey) headers['WMS-Idempotency-Key'] = idempotencyKey;
return fetch(url, {method, headers, body: body === undefined ? undefined : JSON.stringify(body)});
};
let res = await send();
if (res.status === 401) {
// The access token expired or was revoked: sign in again and repeat once.
jwt = null;
res = await send();
}
const text = await res.text();
let data = text;
try {
data = text ? JSON.parse(text) : null;
} catch {
// Some errors come back as plain text.
}
if (!res.ok) throw new WmsError(res.status, data);
return {status: res.status, headers: res.headers, body: data};
}
# WMS integration client: exchanges the technical token for a short-lived
# access token (JWT) and sends authorized requests. Requires `requests`.
import os
import time
from datetime import datetime
import requests
CONFIG = {
"base_url": os.environ["WMS_BASE_URL"].rstrip("/"), # e.g. https://app.wms.com.ua
"tenant": os.environ["WMS_TENANT"], # company code
"token": os.environ["WMS_TOKEN"], # technical token issued in the WMS Client
"external_system": os.environ["WMS_EXTERNAL_SYSTEM"], # external system code
}
_session = requests.Session()
_jwt = {"access_token": None, "renew_at": 0.0}
class WmsError(Exception):
def __init__(self, status, body):
details = ""
if isinstance(body, dict):
details = "; ".join(f"{e.get('path') or '(request)'}: {e.get('message')}" for e in body.get("errors", []))
body = body.get("message")
super().__init__(f"WMS HTTP {status}: {body}" + (f" — {details}" if details else ""))
self.status = status
def get_access_token():
if _jwt["access_token"] and time.time() < _jwt["renew_at"]:
return _jwt["access_token"]
res = _session.post(
f"{CONFIG['base_url']}/api/v1/auth/technical/jwt",
headers={"Authorization": f"Bearer {CONFIG['token']}", "WMS-Tenant-Code": CONFIG["tenant"]},
)
if not res.ok:
raise WmsError(res.status_code, res.text)
data = res.json()
expires_at = datetime.fromisoformat(data["expiresAt"].replace("Z", "+00:00")).timestamp()
# Renew 30 seconds before the token expires.
_jwt.update(access_token=data["accessToken"], renew_at=expires_at - 30)
return _jwt["access_token"]
def wms_request(method, path, *, query=None, body=None, idempotency_key=None):
def send():
headers = {
"Authorization": f"Bearer {get_access_token()}",
"WMS-Tenant-Code": CONFIG["tenant"],
"Accept": "application/json",
}
if idempotency_key:
headers["WMS-Idempotency-Key"] = idempotency_key
# A list in `query` becomes repeated parameters: entityTypes=A&entityTypes=B
return _session.request(method, CONFIG["base_url"] + path, params=query, json=body, headers=headers)
res = send()
if res.status_code == 401:
# The access token expired or was revoked: sign in again and repeat once.
_jwt["access_token"] = None
res = send()
if not res.ok:
try:
error = res.json()
except ValueError:
error = res.text # some errors come back as plain text
raise WmsError(res.status_code, error)
return res
// WMS integration client for 1C:Enterprise 8.3 (server common module).
// Exchanges the technical token for a short-lived access token (JWT) and sends
// authorized requests. Common modules keep no state between calls, so the
// settings and the cached access token travel in the Подключение structure.
Функция НовоеПодключение(АдресСервера, КодКомпании, ТехническийТокен, КодВнешнейСистемы) Экспорт
// АдресСервера - e.g. "https://app.wms.com.ua"; ТехническийТокен - "<your-token>".
Подключение = Новый Структура;
Подключение.Вставить("АдресСервера", АдресСервера);
Подключение.Вставить("КодКомпании", КодКомпании);
Подключение.Вставить("ТехническийТокен", ТехническийТокен);
Подключение.Вставить("КодВнешнейСистемы", КодВнешнейСистемы);
Подключение.Вставить("ТокенДоступа", "");
Подключение.Вставить("ОбновитьПосле", '00010101'); // UTC
Возврат Подключение;
КонецФункции
Функция ПолучитьТокенДоступа(Подключение) Экспорт
Если ЗначениеЗаполнено(Подключение.ТокенДоступа)
И ТекущаяУниверсальнаяДата() < Подключение.ОбновитьПосле Тогда
Возврат Подключение.ТокенДоступа;
КонецЕсли;
Заголовки = Новый Соответствие;
Заголовки.Вставить("Authorization", "Bearer " + Подключение.ТехническийТокен);
Заголовки.Вставить("WMS-Tenant-Code", Подключение.КодКомпании);
Ответ = ОтправитьHTTP(Подключение, "POST", "/api/v1/auth/technical/jwt", Заголовки, "");
Если Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение "WMS: вход не выполнен (" + Ответ.КодСостояния + "): " + Ответ.Текст;
КонецЕсли;
Данные = ПрочитатьОтветJSON(Ответ.Текст);
Подключение.ТокенДоступа = Данные["accessToken"];
// "2026-10-27T10:15:00.123Z" -> UTC date; renew 30 seconds before expiry.
Истекает = XMLЗначение(Тип("Дата"), Лев(Данные["expiresAt"], 19));
Подключение.ОбновитьПосле = Истекает - 30;
Возврат Подключение.ТокенДоступа;
КонецФункции
// Returns Структура(КодСостояния, Заголовки, Тело): Тело is the parsed JSON
// (Соответствие / Массив) or Неопределено. Raises an exception on a non-2xx answer.
Функция ВыполнитьЗапросWMS(Подключение, Метод, Путь, ПараметрыЗапроса = Неопределено,
Тело = Неопределено, КлючИдемпотентности = "") Экспорт
ПолныйПуть = Путь + СтрокаПараметров(ПараметрыЗапроса);
ТекстТела = "";
Если Тело <> Неопределено Тогда
Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, Тело);
ТекстТела = Запись.Закрыть();
КонецЕсли;
Ответ = ОтправитьАвторизованно(Подключение, Метод, ПолныйПуть, ТекстТела, КлючИдемпотентности);
Если Ответ.КодСостояния = 401 Тогда
// The access token expired or was revoked: sign in again and repeat once.
Подключение.ТокенДоступа = "";
Ответ = ОтправитьАвторизованно(Подключение, Метод, ПолныйПуть, ТекстТела, КлючИдемпотентности);
КонецЕсли;
Если Ответ.КодСостояния < 200 Или Ответ.КодСостояния >= 300 Тогда
ВызватьИсключение "WMS HTTP " + Ответ.КодСостояния + ": " + Ответ.Текст;
КонецЕсли;
Результат = Новый Структура("КодСостояния, Заголовки, Тело", Ответ.КодСостояния, Ответ.Заголовки);
Если ЗначениеЗаполнено(Ответ.Текст) Тогда
Результат.Тело = ПрочитатьОтветJSON(Ответ.Текст);
КонецЕсли;
Возврат Результат;
КонецФункции
Функция ОтправитьАвторизованно(Подключение, Метод, ПолныйПуть, ТекстТела, КлючИдемпотентности)
Заголовки = Новый Соответствие;
Заголовки.Вставить("Authorization", "Bearer " + ПолучитьТокенДоступа(Подключение));
Заголовки.Вставить("WMS-Tenant-Code", Подключение.КодКомпании);
Заголовки.Вставить("Accept", "application/json");
Если ЗначениеЗаполнено(ТекстТела) Тогда
Заголовки.Вставить("Content-Type", "application/json");
КонецЕсли;
Если ЗначениеЗаполнено(КлючИдемпотентности) Тогда
Заголовки.Вставить("WMS-Idempotency-Key", КлючИдемпотентности);
КонецЕсли;
Возврат ОтправитьHTTP(Подключение, Метод, ПолныйПуть, Заголовки, ТекстТела);
КонецФункции
Функция ОтправитьHTTP(Подключение, Метод, ПолныйПуть, Заголовки, ТекстТела)
Адрес = Подключение.АдресСервера;
ЗащищенноеСоединение = Неопределено;
Если СтрНачинаетсяС(НРег(Адрес), "https://") Тогда
// Verify the server certificate against the trusted roots of the operating system.
ЗащищенноеСоединение = Новый ЗащищенноеСоединениеOpenSSL(, Новый СертификатыУдостоверяющихЦентровОС);
Адрес = Сред(Адрес, 9);
Иначе
Адрес = СтрЗаменить(Адрес, "http://", "");
КонецЕсли;
// Host[:port] and an optional base path, e.g. "app.wms.com.ua:8443/wms".
ПозицияПути = СтрНайти(Адрес, "/");
БазовыйПуть = ?(ПозицияПути > 0, СокрП(Сред(Адрес, ПозицияПути)), "");
Хост = ?(ПозицияПути > 0, Лев(Адрес, ПозицияПути - 1), Адрес);
Порт = Неопределено;
Если СтрНайти(Хост, ":") > 0 Тогда
Порт = Число(Сред(Хост, СтрНайти(Хост, ":") + 1));
Хост = Лев(Хост, СтрНайти(Хост, ":") - 1);
КонецЕсли;
Если СтрЗаканчиваетсяНа(БазовыйПуть, "/") Тогда
БазовыйПуть = Лев(БазовыйПуть, СтрДлина(БазовыйПуть) - 1);
КонецЕсли;
Соединение = Новый HTTPСоединение(Хост, Порт, , , , 180, ЗащищенноеСоединение);
Запрос = Новый HTTPЗапрос(БазовыйПуть + ПолныйПуть, Заголовки);
Если ЗначениеЗаполнено(ТекстТела) Тогда
Запрос.УстановитьТелоИзСтроки(ТекстТела, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
КонецЕсли;
HTTPОтвет = Соединение.ВызватьHTTPМетод(Метод, Запрос);
Возврат Новый Структура("КодСостояния, Заголовки, Текст",
HTTPОтвет.КодСостояния, HTTPОтвет.Заголовки, HTTPОтвет.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
КонецФункции
Функция ПрочитатьОтветJSON(Текст) Экспорт
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(Текст);
// Read into Соответствие: field names such as "_wmsId" or "int" are kept as is.
Результат = ПрочитатьJSON(Чтение, Истина);
Чтение.Закрыть();
Возврат Результат;
КонецФункции
Функция СтрокаПараметров(ПараметрыЗапроса)
Если ПараметрыЗапроса = Неопределено Тогда
Возврат "";
КонецЕсли;
Части = Новый Массив;
Для Каждого Параметр Из ПараметрыЗапроса Цикл
// An array becomes repeated parameters: entityTypes=A&entityTypes=B
Значения = Параметр.Значение;
Если ТипЗнч(Значения) <> Тип("Массив") Тогда
Значения = Новый Массив;
Значения.Добавить(Параметр.Значение);
КонецЕсли;
Для Каждого Значение Из Значения Цикл
// Numbers without group separators, otherwise 12345 becomes "12 345".
ТекстЗначения = ?(ТипЗнч(Значение) = Тип("Число"), Формат(Значение, "ЧГ=0"), Строка(Значение));
Части.Добавить(Параметр.Ключ + "=" + КодироватьСтроку(ТекстЗначения, СпособКодированияСтроки.КодировкаURL));
КонецЦикла;
КонецЦикла;
Возврат ?(Части.Количество() = 0, "", "?" + СтрСоединить(Части, "&"));
КонецФункции
Errors
A request that the server rejects as a whole comes back with a 4xx status. Most errors carry a JSON body:
{
"message": "INVALID_DATA_FORMAT",
"errors": [
{"path": "code", "message": "External system with code NOPE not found"}
]
}
path points at the field that caused the problem (an empty path means the request as a whole). Some errors come back as plain text instead, for example Limit must be between 1 and 500, so do not assume the body is always JSON. The error texts follow the Accept-Language header.
| Status | Meaning | What to do |
|---|---|---|
| 400 | The request is invalid: a wrong field, a value out of range, an unknown external system code. | Fix the request; repeating it unchanged gives the same answer. |
| 401 | The access token is missing, expired or revoked. | Sign in again and repeat once. |
| 403 | Access is denied: the external system is switched off or lacks a capability, the company code does not match, or the licence has no free technical seats. | Ask the administrator to check the external system and the technical user. |
| 404 | The address does not exist. | Check the path. |
| 429 | Too many requests in a short time. | Wait (the answer says how long) and repeat. |
| 5xx | A failure on the server side. | Wait and repeat; for an import, repeat with the same idempotency key. |
An import is different: the server answers 200 even when some of the items fail, and reports each item separately; see Import.