Электронная транспортная накладная из вашей ТМС — по одному API
Т1-API принимает подписанный титул, доводит его до оператора ЭДО, следит за перевозкой и присылает события обратно в вашу систему. Оператор, роуминг, форматы ФНС и учётки — на стороне платформы: контракт для всех операторов один.
Обзор #
Электронная транспортная накладная (ЭТрН) живёт не в одной системе: грузоотправитель подписывает титул Т1, перевозчик и водитель — свои титулы, обмен идёт через операторов ЭДО, а стороны сделки часто сидят у разных операторов. Т1-API убирает эту машинерию из вашего кода.
Один контракт на всех операторов
Контур, СберКорус, Астрал, СБИС. Маршрут выбирает платформа — или вы, кодом оператора.
Роуминг не ваша забота
Контрагент у другого оператора — накладная всё равно дойдёт. Достижимость проверяется заранее.
Титулы Т1–Т9 целиком
Вся цепочка перевозки, включая переадресовку и замену водителя, в одной карточке накладной.
Песочница по ключу
Тестовый ключ — и тот же боевой URL, но с моками операторов. Полный цикл до delivered за минуты.
Что вам нужно на своей стороне
- Титул Т1 в формате ФНС 5.01 (файл в кодировке windows-1251) и отсоединённая КЭП к нему — CMS/PKCS7. Формировать титул и подписывать вы можете чем угодно: своим СКЗИ, КриптоПро, сервисом подписи.
- API-ключ из веб-кабинета организации.
- HTTPS-эндпоинт для вебхуков — если хотите узнавать о событиях сразу, а не поллингом.
Водительская часть цепочки (Т2, Т4 — приём груза и выдача) подписывается в мобильном приложении «Куб Транспорт» и появляется в карточке накладной автоматически. Вашей интеграции для этого делать не нужно ничего.
Быстрый старт #
Пять шагов от нуля до первой накладной в песочнице.
1. Выпустите ключ
Веб-кабинет cabinet.etrn.app → Настройки → API → «Выпустить ключ». Дайте ключу название («1С:Бухгалтерия, прод») — через полгода это единственный способ понять, какой ключ где работает. Начните с тестового: он включает песочницу целиком.
Значение подставится во все примеры на странице. Оно остаётся в вашем браузере: ни на сервер, ни в аналитику не уходит — на этой странице их просто нет.
2. Проверьте ключ одним запросом
GET /organization вернёт организацию, тариф и остаток лимита — заодно убедитесь, что ключ живой.
curl -sS https://api.etrn.app/v1/organization \
-H "Authorization: Bearer $ETRN_API_KEY"import requests
BASE_URL = "https://api.etrn.app/v1"
API_KEY = "etrn_test_…"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {API_KEY}"
org = session.get(f"{BASE_URL}/organization", timeout=30).json()
print(org["name"], org["tariff"]["documents_left"])req, _ := http.NewRequest(http.MethodGet, "https://api.etrn.app/v1/organization", nil)
req.Header.Set("Authorization", "Bearer "+apiKey) // apiKey — etrn_test_…
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
if err != nil {
return err
}
defer resp.Body.Close()Соединение = Новый HTTPСоединение("api.etrn.app", 443, , , , 30,
Новый ЗащищенноеСоединениеOpenSSL);
Запрос = Новый HTTPЗапрос("/v1/organization");
Запрос.Заголовки.Вставить("Authorization", "Bearer " + КлючAPI); // etrn_test_…
Ответ = Соединение.Получить(Запрос);
Сообщить(Ответ.ПолучитьТелоКакСтроку());3. Подготовьте титул
Ядро v1 принимает готовую пару: XML титула Т1 (ФНС 5.01, windows-1251) и отсоединённую подпись CMS/PKCS7. Оба передаются строками base64 — кодируйте исходные байты файлов как есть. Если ваше СКЗИ уже отдаёт подпись base64-текстом, не кодируйте её второй раз.
4. Прогоните проверку без подачи
POST /waybills/validate — тот же конвейер проверок, что и при подаче, но ничего не
создаётся и не списывается. Удобно ставить в CI титулогенератора.
jq -n --arg xml "$XML_B64" --arg sig "$SIG_B64" '{
signed_xml: {xml: $xml, signature: $sig},
parties: [
{role: "shipper", inn: "1650152978", kpp: "165001001"},
{role: "carrier", inn: "7713488175"},
{role: "consignee", inn: "7736207543"}
]
}' | curl -sS https://api.etrn.app/v1/waybills/validate \
-H "Authorization: Bearer $ETRN_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-
# → {"valid": true, "problems": []}
# либо valid=false и problems[{field, message}] — что именно не сошлось5. Подайте накладную и прогоните цикл
Дальше — подача и, в песочнице,
simulate: накладная за минуты доходит до delivered,
в цепочке появляются подписанные Т2 и Т4, а ваш обработчик вебхуков получает настоящие события.
Ключи и доступ #
Аутентификация — заголовок Authorization: Bearer <ключ> в каждом запросе.
Базовый URL — https://api.etrn.app/v1, только HTTPS. Версия живёт в базовом пути;
в самих ручках префикса нет.
| Скоуп | Что разрешает |
|---|---|
waybills:read | читать накладные, цепочку документов, QR, справочники |
waybills:write | подавать накладные и проверять титулы |
webhooks:manage | создавать, читать и удалять подписки на события |
Ключ принадлежит организации, а не сотруднику: увольнение человека не гасит интеграцию. Значение показывается один раз при выпуске — платформа хранит только его хэш, «показать ещё раз» невозможно by design.
Выпустите второй ключ → переключите на него интеграцию → отзовите старый. Оба ключа живут параллельно, пока вы переключаетесь. Обратный порядок («отозвать, потом выпустить») останавливает обмен.
Контур ключа виден в самом значении: etrn_live_… — боевой, etrn_test_… —
песочница. Один и тот же код работает с обоими: меняется только ключ.
Песочница #
Тестовый ключ включает песочницу целиком — отдельного стенда, адреса и настроек не нужно.
- Запросы идут на тот же
https://api.etrn.app/v1, но обслуживаются управляемыми моками операторов: операторsandboxподставляется сам, полеoperatorможно не передавать. POST /waybills/{waybillId}/simulateпродвигает накладную по жизненному циклу без реального водителя и оператора — включая появление подписанных Т2/Т4 вdocuments[].- Вебхуки приходят по-настоящему: обработчик отлаживается на полной цепочке событий.
- Ни один боевой документ не расходуется и ни один рубль не списывается.
# Прогнать цепочку до конца — события придут в вебхук как в бою
curl -sS "https://api.etrn.app/v1/waybills/$WAYBILL_ID/simulate" \
-H "Authorization: Bearer $ETRN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"advance_to": "delivered"}'
advance_to принимает любой статус жизненного цикла; переход назад невозможен (409).
В боевом контуре ручки simulate нет вовсе — с ключом etrn_live_… она
отвечает 404.
Подача ЭТрН #
POST /v1/waybills
принимает подписанный титул Т1 и стороны сделки. Ответ — объект Waybill:
201, если накладная принята, 200 — если это повтор по
Idempotency-Key.
Поле operator необязательно: не передали — оператора выберет платформа по правилу
организации. Хотите управлять сами — передайте код из GET /operators;
неизвестный или выключенный код отклоняется с 422.
# Титул и отсоединённая подпись → base64 одной строкой
XML_B64=$(base64 < t1.xml | tr -d '\n') # титул ФНС 5.01, windows-1251
SIG_B64=$(base64 < t1.xml.sig | tr -d '\n') # КЭП CMS/PKCS7 (DER)
jq -n --arg xml "$XML_B64" --arg sig "$SIG_B64" '{
signed_xml: {xml: $xml, signature: $sig},
parties: [
{role: "shipper", inn: "1650152978", kpp: "165001001"},
{role: "carrier", inn: "7713488175"},
{role: "consignee", inn: "7736207543"}
],
external_ref: "TMS-2026-000123"
}' | curl -sS https://api.etrn.app/v1/waybills \
-H "Authorization: Bearer $ETRN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @-"""Подача ЭТрН (титул Т1). Зависимости: pip install requests."""
import base64, uuid, requests
BASE_URL = "https://api.etrn.app/v1"
API_KEY = "etrn_test_…"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {API_KEY}"
class EtrnProblem(Exception):
"""Ошибка API в формате RFC 9457 (application/problem+json)."""
def __init__(self, status: int, problem: dict):
self.status = status
self.type = problem.get("type", "about:blank") # машинный классификатор
self.detail = problem.get("detail", "")
self.request_id = problem.get("request_id", "") # в обращение в поддержку
self.errors = problem.get("errors", []) # пополевые детали при 422
super().__init__(f"{status} {self.detail} (request_id={self.request_id})")
def raise_for_problem(resp: requests.Response) -> None:
if resp.ok:
return
if resp.headers.get("Content-Type", "").startswith("application/problem+json"):
raise EtrnProblem(resp.status_code, resp.json())
resp.raise_for_status()
def submit_waybill(xml_path: str, sig_path: str) -> dict:
with open(xml_path, "rb") as f: # титул ФНС 5.01, windows-1251
xml_bytes = f.read()
with open(sig_path, "rb") as f: # отсоединённая КЭП (CMS/PKCS7, DER)
sig_bytes = f.read()
resp = session.post(
f"{BASE_URL}/waybills",
json={
"signed_xml": {
"xml": base64.b64encode(xml_bytes).decode(),
"signature": base64.b64encode(sig_bytes).decode(),
},
"parties": [
{"role": "shipper", "inn": "1650152978", "kpp": "165001001"},
{"role": "carrier", "inn": "7713488175"},
{"role": "consignee", "inn": "7736207543"},
],
"external_ref": "TMS-2026-000123", # номер рейса у вас
},
headers={"Idempotency-Key": str(uuid.uuid4())}, # безопасный повтор 24 ч
timeout=30,
)
raise_for_problem(resp) # 402 — лимит тарифа, 422 — титул не прошёл
return resp.json() # 201 — принята, 200 — повтор по ключу идемпотентности// Подача ЭТрН (титул Т1) — стандартная библиотека, без зависимостей.
const baseURL = "https://api.etrn.app/v1"
type SignedXML struct {
XML string `json:"xml"` // титул ФНС 5.01 (windows-1251), base64
Signature string `json:"signature"` // отсоединённая КЭП CMS/PKCS7, base64
}
type Party struct {
Role string `json:"role"` // shipper | carrier | consignee
INN string `json:"inn"`
KPP string `json:"kpp,omitempty"`
}
// Problem — ошибка API в формате RFC 9457.
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Detail string `json:"detail"`
RequestID string `json:"request_id"`
}
func (p Problem) Error() string {
return fmt.Sprintf("%s: %s (request_id=%s)", p.Title, p.Detail, p.RequestID)
}
func submitWaybill(apiKey string, xml, sig []byte) (*Waybill, error) {
payload, err := json.Marshal(CreateWaybillRequest{
SignedXML: SignedXML{
XML: base64.StdEncoding.EncodeToString(xml),
Signature: base64.StdEncoding.EncodeToString(sig),
},
Parties: []Party{
{Role: "shipper", INN: "1650152978", KPP: "165001001"},
{Role: "carrier", INN: "7713488175"},
{Role: "consignee", INN: "7736207543"},
},
ExternalRef: "TMS-2026-000123",
})
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, baseURL+"/waybills", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey()) // повтор безопасен 24 ч
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 201 — принята, 200 — повтор по Idempotency-Key
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
var p Problem // 402 — лимит тарифа, 422 — титул не прошёл проверку
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
return nil, p
}
var wb Waybill
if err := json.NewDecoder(resp.Body).Decode(&wb); err != nil {
return nil, err
}
return &wb, nil
}// Подача ЭТрН (титул Т1). Платформа 8.3.
Функция Базовый64(ПутьКФайлу)
// Base64Строка вставляет переводы строк — для JSON их нужно убрать
Строка64 = Base64Строка(Новый ДвоичныеДанные(ПутьКФайлу));
Строка64 = СтрЗаменить(Строка64, Символы.ВК, "");
Строка64 = СтрЗаменить(Строка64, Символы.ПС, "");
Возврат Строка64;
КонецФункции
Функция ПодатьНакладную(КлючAPI, ПутьКXML, ПутьКПодписи)
ПодписанныйXML = Новый Структура;
ПодписанныйXML.Вставить("xml", Базовый64(ПутьКXML)); // ФНС 5.01, windows-1251
ПодписанныйXML.Вставить("signature", Базовый64(ПутьКПодписи)); // КЭП CMS/PKCS7
Стороны = Новый Массив;
Грузоотправитель = Новый Структура("role, inn, kpp", "shipper", "1650152978", "165001001");
Стороны.Добавить(Грузоотправитель);
Перевозчик = Новый Структура("role, inn", "carrier", "7713488175");
Стороны.Добавить(Перевозчик);
Грузополучатель = Новый Структура("role, inn", "consignee", "7736207543");
Стороны.Добавить(Грузополучатель);
Тело = Новый Структура;
Тело.Вставить("signed_xml", ПодписанныйXML);
Тело.Вставить("parties", Стороны);
Тело.Вставить("external_ref", "TMS-2026-000123"); // номер рейса в вашей системе
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, Тело);
СтрокаТела = ЗаписьJSON.Закрыть();
Запрос = Новый HTTPЗапрос("/v1/waybills");
Запрос.Заголовки.Вставить("Authorization", "Bearer " + КлючAPI);
Запрос.Заголовки.Вставить("Content-Type", "application/json");
// Безопасный повтор 24 ч: тот же ключ вернёт исходный результат
Запрос.Заголовки.Вставить("Idempotency-Key", Строка(Новый УникальныйИдентификатор));
Запрос.УстановитьТелоИзСтроки(СтрокаТела, КодировкаТекста.UTF8);
Соединение = Новый HTTPСоединение("api.etrn.app", 443, , , , 30,
Новый ЗащищенноеСоединениеOpenSSL);
Ответ = Соединение.ОтправитьДляОбработки(Запрос);
СтрокаОтвета = Ответ.ПолучитьТелоКакСтроку();
// 201 — принята, 200 — повтор. Иначе — problem+json: type/detail/request_id
Если Ответ.КодСостояния <> 201 И Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение "eTRN " + Ответ.КодСостояния + ": " + СтрокаОтвета;
КонецЕсли;
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаОтвета);
Возврат ПрочитатьJSON(ЧтениеJSON, Истина);
КонецФункцииОтвет
{
"id": "0b3c6c1e-8b7e-4f7a-9d2e-5a1f1c9e4b7d",
"status": "pending_receipt",
"number": "ТрН-2026-000123",
"date": "2026-08-16",
"external_ref": "TMS-2026-000123",
"operator": "kontur",
"created_at": "2026-08-16T10:21:33Z"
}Передавайте Idempotency-Key при каждой подаче. Повтор с тем же ключом в течение
24 часов вернёт исходный результат (200 вместо 201) — сеть, таймаут и рестарт вашей очереди
не превратятся во вторую накладную. Тот же ключ с другим телом — 409.
Статусы перевозки #
| Статус | Что означает |
|---|---|
submitting | оператор временно перегружен, платформа досылает титул сама — ретраи на вашей стороне не нужны |
pending_receipt | титул у оператора, ждём приёма груза водителем (Т2) |
in_transit | груз принят, машина в пути |
pending_delivery | прибытие к грузополучателю, ждём выдачи (Т4) |
delivered | перевозка завершена, цепочка титулов закрыта |
failed | терминальный: причина в failure {code, message}, списанная подпись возвращается — исправленный титул подаётся новой накладной |
cancelled | стороны аннулировали поток на стороне оператора |
При обычной синхронной подаче submitting вы не увидите — ответ сразу
pending_receipt. Статус awaiting_signature относится к версии v1.1
(черновик + двухфазная подпись) и в ядре не встречается.
Титулы Т1–Т9 #
Все титулы перевозки приезжают в documents[] карточки накладной — с XML, подписью и
временем. Вам не нужно знать, какой оператор их принёс и в каком формате они лежали у него.
| Титул | Кто подписывает | Когда появляется |
|---|---|---|
T1 | грузоотправитель | ваша подача — начало цепочки |
T2 | водитель (приём груза) | погрузка; в приложении «Куб Транспорт» |
T3 | грузоотправитель | подтверждение приёма груза к перевозке |
T4 | водитель (выдача груза) | выгрузка у грузополучателя |
T5 | грузополучатель | приём груза получателем |
T6 | грузоотправитель / перевозчик | уточнение условий перевозки |
T7 | перевозчик | переадресовка — смена пункта выгрузки |
T8 | перевозчик | замена водителя или транспортного средства |
T9 | стороны | служебные отметки цепочки |
Водительские титулы подписываются в мобильном приложении, титулы контрагентов — у них.
Ваша интеграция подаёт Т1 и читает остальное: подписка на
waybill.document_added сообщит о каждом новом документе цепочки.
Документы и QR #
GET /v1/waybills/{waybillId}
отдаёт карточку с массивом documents[]. Содержимое каждого документа скачивается
отдельно — XML или отсоединённая подпись:
WAYBILL_ID=$(jq -r .id waybill.json)
# Карточка: статус + цепочка документов (Т2/Т4 водителя появятся здесь же)
curl -sS "https://api.etrn.app/v1/waybills/$WAYBILL_ID" \
-H "Authorization: Bearer $ETRN_API_KEY"
# XML документа цепочки и отсоединённая подпись к нему
curl -sS "https://api.etrn.app/v1/waybills/$WAYBILL_ID/documents/$DOC_ID/content?part=xml" \
-H "Authorization: Bearer $ETRN_API_KEY" -o t2.xml
curl -sS "https://api.etrn.app/v1/waybills/$WAYBILL_ID/documents/$DOC_ID/content?part=sig" \
-H "Authorization: Bearer $ETRN_API_KEY" -o t2.xml.sig
# QR подтверждения перевозки — показать инспектору на дороге (PNG)
curl -sS "https://api.etrn.app/v1/waybills/$WAYBILL_ID/qr" \
-H "Authorization: Bearer $ETRN_API_KEY" -o qr.pngВебхуки #
Подписка — POST /webhooks с url (только https) и списком событий.
Пустой список означает «все события». secret возвращается один раз
в ответе на создание: список подписок секретов не отдаёт. Утеряли — удалите подписку и создайте
новую; это же и штатная ротация секрета.
| Событие | Когда приходит |
|---|---|
waybill.created | накладная принята платформой |
waybill.status_changed | сменился статус: data.previous_status → data.waybill.status |
waybill.document_added | в цепочке новый документ, например подписанный Т2 или Т4 (data.document) |
curl -sS https://api.etrn.app/v1/webhooks \
-H "Authorization: Bearer $ETRN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tms.example.ru/etrn/webhook",
"events": ["waybill.created", "waybill.status_changed", "waybill.document_added"]
}'
# → 201: id, url, events, status, created_at и secret (больше нигде не отдаётся)Правила доставки
- At-least-once. Одно событие может прийти повторно — обработчик обязан быть
идемпотентным по
event.id. - Ретраи. Любой ответ не-2xx повторяется с растущей паузой до 24 часов.
После суток неудач подписка помечается сломанной (
status: broken) и видна такой в кабинете и вGET /webhooks. - Порядок не гарантируется. Ориентируйтесь на
created_atсобытия и текущийstatusнакладной, а не на порядок приходов. - Без вебхуков тоже можно. Поллинг
GET /waybills?created_from=…— законная стратегия для простых интеграций.
Проверка подписи #
Каждая доставка подписана заголовком
X-Etrn-Signature: t=<unix>,v1=<hex(hmac_sha256(secret, t + "." + body))>
Проверяйте подпись до разбора тела и отбрасывайте метки времени старше пяти минут — это защита от повторной отправки перехваченного запроса. Ниже — готовые функции.
"""Приём вебхука eTRN: проверка подписи и идемпотентная обработка."""
import hashlib, hmac, json, time
WEBHOOK_SECRET = "…" # из ответа POST /webhooks — показывается один раз
REPLAY_WINDOW_SECONDS = 300 # 5 минут
def verify_signature(secret: str, header: str, body: bytes) -> bool:
"""X-Etrn-Signature: t=<unix>,v1=<hex(hmac_sha256(secret, t + "." + body))>."""
try:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t, their = parts["t"], parts["v1"]
except (ValueError, KeyError):
return False
if not t.isdigit() or abs(time.time() - int(t)) > REPLAY_WINDOW_SECONDS:
return False # метка времени вне окна — возможен replay
ours = hmac.new(secret.encode(), (t + ".").encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(ours, their) # сравнение за постоянное время
def handle_webhook(headers: dict, body: bytes) -> int:
if not verify_signature(WEBHOOK_SECRET, headers.get("X-Etrn-Signature", ""), body):
return 401
event = json.loads(body)
if is_processed(event["id"]): # at-least-once: доставка может повториться
return 200
waybill = event["data"]["waybill"]
if event["type"] == "waybill.status_changed":
... # обновите рейс: waybill["external_ref"] → waybill["status"]
mark_processed(event["id"])
return 200 # любой 2xx = доставлено, иначе ретраи до 24 чconst replayWindow = 5 * time.Minute // защита от replay
// verifyEtrnSignature проверяет заголовок
// X-Etrn-Signature: t=<unix>,v1=<hex(hmac_sha256(secret, t + "." + body))>.
func verifyEtrnSignature(secret, header string, body []byte) bool {
var ts, v1 string
for _, part := range strings.Split(header, ",") {
if after, ok := strings.CutPrefix(part, "t="); ok {
ts = after
}
if after, ok := strings.CutPrefix(part, "v1="); ok {
v1 = after
}
}
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return false
}
if d := time.Since(time.Unix(t, 0)); d > replayWindow || d < -replayWindow {
return false // метка времени вне окна
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts))
mac.Write([]byte("."))
mac.Write(body)
got, err := hex.DecodeString(v1)
if err != nil {
return false
}
return hmac.Equal(mac.Sum(nil), got) // сравнение за постоянное время
}Тело доставки
{
"id": "evt_2f8c1a5b9d", // ключ идемпотентности
"type": "waybill.status_changed",
"created_at": "2026-08-16T12:04:11Z",
"data": {
"previous_status": "pending_receipt",
"waybill": {
"id": "0b3c6c1e-8b7e-4f7a-9d2e-5a1f1c9e4b7d",
"status": "in_transit",
"external_ref": "TMS-2026-000123"
}
}
}Все ручки #
Полный контракт с телами запросов и схемами ответов — в OpenAPI-спеке.
| Ручка | Назначение | Скоуп |
|---|---|---|
POST /waybills | подать ЭТрН (титул Т1) | waybills:write |
POST /waybills/validate | проверить титул без подачи — ничего не списывается | waybills:write |
GET /waybills | список: фильтры status, number, created_from, created_to; курсор | waybills:read |
GET /waybills/{id} | карточка накладной с цепочкой documents[] | waybills:read |
GET /waybills/{id}/documents/{docId}/content | XML титула или подпись: ?part=xml|sig | waybills:read |
GET /waybills/{id}/qr | QR подтверждения перевозки (PNG) | waybills:read |
POST /waybills/{id}/simulate | только песочница: продвинуть цепочку | waybills:write |
POST /webhooks | создать подписку (в ответе secret — один раз) | webhooks:manage |
GET /webhooks | список подписок со статусом active | broken | webhooks:manage |
DELETE /webhooks/{id} | удалить подписку | webhooks:manage |
GET /organization | организация ключа, тариф, остаток лимита | любой |
GET /operators | справочник операторов ЭДО и их доступность | waybills:read |
GET /counterparties/{inn} | достижим ли контрагент — напрямую или роумингом | waybills:read |
Ошибки #
Все ошибки — в формате RFC 9457 Problem Details (application/problem+json):
type — стабильный URI-классификатор для машинной обработки, title и
detail — человеку, request_id — приложите к обращению в поддержку,
errors[] — пополевые детали при 422. HTTP-статус в теле не дублируется: источник
один — статус ответа.
{
"type": "https://api.etrn.app/errors/tariff-limit",
"title": "Лимит подписей тарифа исчерпан",
"detail": "Использовано 500 из 500 документов периода. Оплата — в кабинете.",
"request_id": "req_9f4c2a7b"
}| Классификатор | HTTP | Когда | Что делать |
|---|---|---|---|
unauthorized | 401 | ключ не передан, неверен или отозван | проверьте заголовок Authorization и статус ключа в кабинете |
insufficient-scope | 403 | у ключа нет нужного скоупа | выпустите ключ со скоупом waybills:write / webhooks:manage |
tariff-limit | 402 | лимит документов тарифа исчерпан | оплата в кабинете; остаток заранее — GET /organization |
validation-failed | 422 | титул или подпись не прошли проверку | детали в errors[]; отладка — POST /waybills/validate |
rate-limited | 429 | превышен лимит запросов ключа | повтор после X-RateLimit-Reset, следите за X-RateLimit-Remaining |
operator-throttled | 503 | оператор ЭДО временно перегружен | повторите позже; принятая накладная досылается платформой сама |
operator-disabled | 503 | оператор отключён платформой | проверьте GET /operators или не передавайте operator вовсе |
Отдельный случай при подаче: явно переданный неизвестный или выключенный код
operator отклоняется как 422 ещё на валидации запроса — до похода к оператору.
Лимиты #
| Лимит | Значение |
|---|---|
| Частота запросов | 10 rps, burst 30 — на ключ. Заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; при превышении 429 |
| Размер запроса подачи | до 2 МБ |
| Идемпотентность | Idempotency-Key до 128 символов, окно 24 ч. Повтор с тем же ключом — исходный результат (200), тот же ключ с другим телом — 409 |
| Пагинация | limit до 200 (по умолчанию 50), курсор next_cursor; пустой курсор — страниц больше нет |
Поддержка #
Пишите на support@etrn.app и прикладывайте
request_id из тела ошибки — по нему запрос находится целиком, вместе с ответом
оператора ЭДО. Если ошибки нет, а поведение странное, приложите время запроса с точностью до
минуты и префикс ключа (etrn_live_a1b2c3d4e…) — полное значение ключа присылать не
нужно никогда.