Основи API
Цей розділ — для розробника, який підключає до WMS облікову чи ERP-систему. Тут описано, як ваш код входить у систему, які заголовки несе кожен запит і як читати відповіді. Наступні сторінки описують кожну операцію обміну.
WMS — хмарний сервіс: усі компанії працюють з одним сервісом за адресою https://app.wms.com.ua, і всі запити цього розділу надсилаються туди. Перед початком адміністратор вашої компанії налаштовує сторону WMS і передає вам три речі — див. контрольний список передачі:
- код компанії (на екрані входу в Клієнт це поле Компанія);
- технічний токен — довгостроковий секрет технічного користувача;
- код зовнішньої системи — код, під яким зареєстровано вашу систему.
Опис API
Сервіс публікує машиночитаний опис усього свого API. Інтеграційні ендпоінти зібрано під тегами, що починаються з Integrations. Опис опубліковано в тестовому середовищі:
- Swagger UI — переглянути та спробувати API
- ReDoc — той самий опис однією сторінкою
- OpenAPI (JSON) — для генераторів клієнтів і Postman
Вхід
Ваш код не надсилає технічний токен з кожним запитом. Він обмінює токен на короткостроковий токен доступу і далі працює з ним:
POST /api/v1/auth/technical/jwt
Authorization: Bearer <технічний токен>
WMS-Tenant-Code: <код компанії>
Тіла в запиті немає. Відповідь:
{
"accessToken": "eyJhbGciOi…",
"expiresAt": "2026-10-27T10:15:00.123Z"
}
- Токен доступу живе близько 10 хвилин. Зберігайте його й використовуйте повторно до моменту незадовго перед
expiresAt, а тоді входьте знову. - Технічний користувач може одночасно мати лише кілька токенів доступу. Вхід перед кожним запитом витісняє токени інших ваших процесів, тож кешуйте токен.
- Якщо запит отримав 401, токен доступу прострочився або його відкликано: увійдіть знову й повторіть запит один раз. Якщо 401 отримав сам вхід, технічний токен неправильний, прострочений або замінений — попросіть адміністратора видати новий (Видача токена).
Заголовки запиту
| Заголовок | Коли | Що містить |
|---|---|---|
Authorization: Bearer <токен доступу> | кожен запит | токен доступу, отриманий під час входу |
WMS-Tenant-Code | кожен запит | код компанії |
Content-Type: application/json | запити з тілом | усі тіла — JSON у кодуванні UTF-8 |
WMS-Idempotency-Key | запити імпорту | унікальний ключ цього запиту, щоб його повторення було безпечним; див. Імпорт |
Accept-Language | за бажанням | мова повідомлень про помилки, наприклад uk або en |
WMS-Correlation-Id | за бажанням | ваш власний ідентифікатор запиту; вкажіть його, коли повідомляєте про проблему |
Клієнт, на якому побудовано приклади
Кожен приклад цього розділу спирається на невеликий клієнт нижче. Він входить у систему, кешує токен доступу, додає заголовки й повторює запит один раз після 401. Версії на JavaScript і Python беруть параметри підключення зі змінних оточення (WMS_BASE_URL, WMS_TENANT, WMS_TOKEN, WMS_EXTERNAL_SYSTEM); версія для 1С зберігає їх у структурі, яку створює НовоеПодключение. Ніколи не вписуйте технічний токен у вихідний код.
- 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, "", "?" + СтрСоединить(Части, "&"));
КонецФункции
Помилки
Запит, який сервер відхиляє повністю, повертається зі статусом 4xx. Більшість помилок мають тіло JSON:
{
"message": "INVALID_DATA_FORMAT",
"errors": [
{"path": "code", "message": "External system with code NOPE not found"}
]
}
path вказує на поле, що спричинило проблему (порожній path означає запит загалом). Деякі помилки натомість повертаються простим текстом, наприклад Limit must be between 1 and 500, тож не розраховуйте, що тіло завжди буде JSON. Тексти помилок залежать від заголовка Accept-Language.
| Статус | Що означає | Що робити |
|---|---|---|
| 400 | Запит некоректний: неправильне поле, значення поза межами, невідомий код зовнішньої системи. | Виправте запит; повтор без змін дасть ту саму відповідь. |
| 401 | Токена доступу немає, він прострочений або відкликаний. | Увійдіть знову й повторіть один раз. |
| 403 | Доступ заборонено: зовнішню систему вимкнено або їй бракує можливості, код компанії не збігається, або в ліцензії немає вільних технічних місць. | Попросіть адміністратора перевірити зовнішню систему й технічного користувача. |
| 404 | Такої адреси не існує. | Перевірте шлях. |
| 429 | Забагато запитів за короткий час. | Зачекайте (відповідь підказує скільки) і повторіть. |
| 5xx | Збій на боці сервера. | Зачекайте й повторіть; для імпорту — з тим самим ключем ідемпотентності. |
З імпортом інакше: сервер відповідає 200, навіть якщо частина елементів не пройшла, і звітує про кожен елемент окремо; див. Імпорт.