Т1-APIКуб Транспорт · eTRN
OpenAPI Кабинет
v1 · боевой контур работает

Электронная транспортная накладная из вашей ТМС — по одному API

Т1-API принимает подписанный титул, доводит его до оператора ЭДО, следит за перевозкой и присылает события обратно в вашу систему. Оператор, роуминг, форматы ФНС и учётки — на стороне платформы: контракт для всех операторов один.

Обзор #

Электронная транспортная накладная (ЭТрН) живёт не в одной системе: грузоотправитель подписывает титул Т1, перевозчик и водитель — свои титулы, обмен идёт через операторов ЭДО, а стороны сделки часто сидят у разных операторов. Т1-API убирает эту машинерию из вашего кода.

Один контракт на всех операторов

Контур, СберКорус, Астрал, СБИС. Маршрут выбирает платформа — или вы, кодом оператора.

Роуминг не ваша забота

Контрагент у другого оператора — накладная всё равно дойдёт. Достижимость проверяется заранее.

Титулы Т1–Т9 целиком

Вся цепочка перевозки, включая переадресовку и замену водителя, в одной карточке накладной.

Песочница по ключу

Тестовый ключ — и тот же боевой URL, но с моками операторов. Полный цикл до delivered за минуты.

Что вам нужно на своей стороне

Водительская часть цепочки (Т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 титулогенератора.

POST /v1/waybills/validate
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_…песочница. Один и тот же код работает с обоими: меняется только ключ.

Песочница #

Тестовый ключ включает песочницу целиком — отдельного стенда, адреса и настроек не нужно.

POST /v1/waybills/{waybillId}/simulate
# Прогнать цепочку до конца — события придут в вебхук как в бою
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, Истина);

КонецФункции

Ответ

201 Created · Waybill
{
  "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 in_transit pending_delivery delivered ·failed cancelled
СтатусЧто означает
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стороныслужебные отметки цепочки
✍️
Подписывать Т2–Т9 из API не нужно

Водительские титулы подписываются в мобильном приложении, титулы контрагентов — у них. Ваша интеграция подаёт Т1 и читает остальное: подписка на waybill.document_added сообщит о каждом новом документе цепочки.

Документы и QR #

GET /v1/waybills/{waybillId} отдаёт карточку с массивом documents[]. Содержимое каждого документа скачивается отдельно — XML или отсоединённая подпись:

Скачать 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_statusdata.waybill.status
waybill.document_addedв цепочке новый документ, например подписанный Т2 или Т4 (data.document)
POST /v1/webhooks
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 (больше нигде не отдаётся)

Правила доставки

Проверка подписи #

Каждая доставка подписана заголовком
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) // сравнение за постоянное время
}

Тело доставки

Event · waybill.status_changed
{
  "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}/contentXML титула или подпись: ?part=xml|sigwaybills:read
GET /waybills/{id}/qrQR подтверждения перевозки (PNG)waybills:read
POST /waybills/{id}/simulateтолько песочница: продвинуть цепочкуwaybills:write
POST /webhooksсоздать подписку (в ответе secret — один раз)webhooks:manage
GET /webhooksсписок подписок со статусом active | brokenwebhooks: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-статус в теле не дублируется: источник один — статус ответа.

402 Payment Required
{
  "type": "https://api.etrn.app/errors/tariff-limit",
  "title": "Лимит подписей тарифа исчерпан",
  "detail": "Использовано 500 из 500 документов периода. Оплата — в кабинете.",
  "request_id": "req_9f4c2a7b"
}
КлассификаторHTTPКогдаЧто делать
unauthorized401ключ не передан, неверен или отозванпроверьте заголовок Authorization и статус ключа в кабинете
insufficient-scope403у ключа нет нужного скоупавыпустите ключ со скоупом waybills:write / webhooks:manage
tariff-limit402лимит документов тарифа исчерпаноплата в кабинете; остаток заранее — GET /organization
validation-failed422титул или подпись не прошли проверкудетали в errors[]; отладка — POST /waybills/validate
rate-limited429превышен лимит запросов ключаповтор после X-RateLimit-Reset, следите за X-RateLimit-Remaining
operator-throttled503оператор ЭДО временно перегруженповторите позже; принятая накладная досылается платформой сама
operator-disabled503оператор отключён платформойпроверьте 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…) — полное значение ключа присылать не нужно никогда.