Всем привет!
Я Антон, системный аналитик из команды трансграничных переводов в Uzum Fintech, и от меня уже два месяца ждут текст по результатам выступления на внутреннем митапе.
Поэтому сегодня мне придется рассказать про то, как с помощью свежего стандарта от OpenApi Initiative можно упростить процесс понимания разных чужих апишек и быстрее интегрироваться с какой-нибудь чужой вундервафлей.
Проблемы с описанием API
Дисклеймер
В данной статье под «интеграцией» будет пониматься скорее какая-то совсем внешняя история. Ребята из соседней команды, которые помогут и всё объяснят с расстояния пары сообщений в телеграме/slack’e тут не очень подходят, но полезно будет и для взаимодействия с ними.
А еще будет полезно разработчикам, тестировщикам, аналитикам, продактам, техписам и даже биздевам — в общем всем, кто так или иначе трогает API.
Усложнение API
Начнем с того, что сейчас очень редко можно встретить интеграцию, в рамках которой достаточно вызвать всего один метод, чтобы получить желаемый бизнес-результат. Чаще приходится дергать в нужной последовательности несколько разных эндпоинтов, причем последовательность может меняться в зависимости от полученного промежуточного результата. То есть происходит переход от «вызова метода» к сценариям.
И тогда для успешной интеграции надо:
Знать, что нужно получить на выходе — бизнесовый результат
Знать сценарий, как достичь этого результата
Знать данные, которые нужны для всего сценария в целом и для каждого шага в частности

Ладно, если бы это было в рамках RESTful API, где глаголы методов и названия ресурсов как-то подсказывают, что делать, но существует классный тренд на смесь REST- и RPC-подходов, где, к сожалению, нет устоявшихся и общепринятых норм: команды придерживаются какой-то одной концепции только в рамках одного конкретного сервиса.
Или, если рассмотреть абстрактный кровавый энтерпрайз, может возникнуть что-то очень похожее на нулевой уровень зрелости REST API по Ричардсону, где все операции делаются через один-два универсальных POST-эндпоинта, но выполняемое действие зависит от передаваемой в теле запроса «команды» и данных. Команд при этом может быть много, и все они требуют абсолютно разных наборов данных.
Во всё это легко вникнуть, если читатель документации — сам автор, в других же случаях явно будут вопросы разной степени глубины и отчаяния.
Усложнение самих API — это только половина беды.
Документация
В отношении документации тоже не все так хорошо, как хотелось бы видеть в 2к26. В лучшем случае она вообще существует. А дальше начинаются всякие разные ухищрения или даже извращения.
То с чем приходилось сталкиваться на практике (без имен):
Всеобъемлющий портал с документацией на все эндпоинты, с описанием структур данных, enum’ов, схем и кучей текста.
Это очень тяжело поддерживать в актуальном состоянии и в любом случае остаются вопросы, как работает та или иная штука в таком-то (именно вашем) кейсе, которые приходится решать через поддержку. Но это скорее около-госовая история и то, к чему надо стремитьсяДокументация на весь сервис в виде .PDF или .docx.
Тут всё по классике: листинги запросов и ответов даны в две колонки для экономии места, но всё равно не влезают на одну страницу. Сразу возрастает сложность при копировании нужного куска JSON’aКоллекция запросов для Postman’a, к которой нет сопроводительного текста, где описана структура папок и что же в конечном счете делает или должен делать каждый из запросов. Или этот текст есть, но вам его не прислали. Или прислали, но через 2 года
И два вспомогательных артефакта:
Sequence-диаграмма на пару экранов по высоте и ширине, любимая большинством аналитиков, и которая не предназначена для хранения всей необходимой информации (типа тел запросов)
OpenApi Specification — формализованное описание API, которое, к счастью, стало стандартом в индустрии
У нас в Uzum Fintech используется комбинированный подход: есть портал разработчика, куда выкладывается документация, собранная по принципу комбинирования лучшего из всех вышеописанных подходов и артефактов.
Пользуясь случаем, передаю привет Дане и коллегам, ответственным за портал.
Подытожу: API усложняются, приходится мыслить сценариями, а не методами, и документация не всегда упрощает процесс интеграции.
Ковры
Ребята из OpenApi Initiative осознали масштаб этой проблемы и выпустили стандарт Arazzo: в переводе с итальянского «гобелен». Мне, для простоты, больше нравится слово «ковёр», поэтому я буду использовать его)
Arazzo — это описание сценария взаимодействия API на формализованном языке. То есть и человеку будет понятно, и этому вашему бездушному ИИ-агенту.
Простейший пример
Рассмотрим простейший пример файла-сценария, который описывает вызов одного метода сервиса моей команды, чтобы разобраться, из чего состоит такой файлик. На нем же поймем тесную связь Arazzo-сценария и спецификации OAS.
arazzo: '1.0.1' info: title: CBT – /convert version: '0.5.1' sourceDescriptions: - name: cbtApi type: openapi url: https://%адрес_со_спекой%/ru_crossborder.yaml workflows: - workflowId: example summary: /convert parameters: - name: Authorization in: header value: Basic значение_токена steps: # Шаг 1 — /convert (НЕ обязателен к успеху) - stepId: call-convert description: POST /convert (optional) operationId: cbtApi.convert requestBody: payload: amount: 50000 currencyFrom: RUB currencyTo: UZS direction: TO_UZ # "Широкий" успех, чтобы не валить сценарий на 4xx successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403
Структура и синтаксис Arazzo-файла очень похожа на описание API в OAS/swagger — это тоже *.YAML с разными блоками.
Пробежимся сверху вниз по этим блокам:
Версия стандарта. Совсем недавно релизнули версию 1.1.0 с поддержкой AsyncAPI, то есть в рамках сценария работы можно будет описывать взаимодействие c Kafka
Информация о самом документе — это больше для читателей-людей
Ссылка на файл спецификации API. Одна из самых важных штук, благодаря которой всё работает. Можно указать как внешней ссылкой, так и локальным файлом. В этом случае я как раз ссылаюсь на наш портал
Блок “workflows” — непосредственно описание вызова методов.
Workflow — это фактически сценарий работы API, в одном файле можно описать несколько сценариев, если хочетсяКак и в OAS в запрос можно добавить параметры, в данном случае заголовок авторизации, и сразу передать желаемое значение base64 от логина и пароля учетки с препрода
Сценарий состоит из шагов. Здесь шаг всего один, для наглядности. Подробность описания и комментарии — на совести автора, как в OAS
stepId — идентификатор шага в рамках сценария
operationId, вторая ключевая вещь, должен совпадать с operationId вызываемого метода из спецификации
Далее — тело запроса, то есть требуемые в рамках шага данные
И в конце — критерии успеха. В примере любой ответ считаем успешным
Тут будет небольшое отступление.
Внимательный читатель заметит, что комментарии в листинге выше явно написаны не автором данной статьи, и окажется прав: весь файл сценария был написан ChatGPT, или, как я его называю, Валерой.
У Arazzo и Валеры давняя история взаимодействия: как только OAI выкатили первую публичную версию стандарта, они сразу рекомендовали использовать допиленную версию чата — Arazzo Specification.
Собственно, все файлы сценариев, которые я когда-либо использовал, написаны Валерой, я их немного редактировал, чтобы они действительно выполняли нужное.

Вернемся к сценариям.
Как будто в вызове одного метода нет ничего сложного. А если хочется в сценарии использовать несколько методов, то возникает логичный вопрос, как вытащить данные, например, из ответа шага №1 и запихнуть в тело запроса в шаг №3?
OAI это предусмотрели, и стандарт сразу поддерживает блок “outputs”:
- stepId: call-convert description: POST /convert (optional) operationId: cbtApi.convert requestBody: payload: amount: 1000000 currencyFrom: UZS currencyTo: RUB direction: FROM_UZ successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 outputs: amountFrom: $response.body#/amountFrom currencyFrom: $response.body#/currencyFrom amountTo: $response.body#/amountTo currencyTo: $response.body#/currencyTo run_id: $response.header.Date # ← внешний id для следующих шагов onFailure: - name: continueToCardListEvenIfConvertFails type: goto stepId: card-list
Тут я сохраняю данные из ответа метода /convert в «переменные». Валера подсказал, что можно сохранить в переменную значение заголовка и решить, например, проблему с созданием всяких уникальных идентификаторов.
Еще добавился блок “onFailure” — что делать, если произошёл неуспех. В данном случае Валера, перемудрив с неймингом, с помощью goto отправляет на другой шаг. Ну, и собственно всё, этой базы должно хватать.
Всякие подробности можно глянуть в спеке.
Полный пример ковра с тремя сценариями и несколькими шагами в каждом я приложу файлом в конце статьи.
Инструменты
Файлики со сценариями это, конечно, хорошо, но делать-то с ними что?
Тут второе отступление: у меня была задачка — описать работу с новым сервисом для партнёров. Наученный сложной коммуникацией с нерусскоязычными партнерами, я сразу вспомнил про Arazzo и начал консультироваться с Валерой, как же все это красиво и быстро запилить, чтобы легко пошарить в виде доки.
Валера меня обманул. Он заявил, что redocly-cli (мы его обычно используем для оформления доки, которую надо пошарить) отлично справляется с визуализацией Arazzo-сценариев прямо рядом с описанием методов спеки. Но в процессе я узнал про их инструмент respect, который тоже можно юзать из того же cli.
Расскажу подробнее именно про redocly respect, потому что про Arazzo UI и Arazzo Editor, дефолтные инструменты для работы со стандартом от самих создателей (тут привет Фрэнку из Jentic), вы скорее всего уже знаете или узнаете при первом походе в гугл.
Отступление три. Когда-то давно, когда я только постигал азы системного анализа и мой работодатель-аутсорсер отправил меня в аутстафф, архитектор из команды заказчика сказал, что Ubuntu — плохая операционная система для аналитика. Поэтому ниже будет рассказ про использование redocly respect под Windows в дефолтной CMD.
Сначала ставим redocly, потому что npm уже установлен.
Официальный полный гайд — тут.
npm i @redocly/cli@latest
После этого можно сразу запускать сценарий:
redocly respect "C:\arazzo\simple_example.yml" --workflow example --server cbtApi=https://%адрес_препрода% --verbose
Тут всё просто:
redocly respect— что вообще запускаемДалее указываем абсолютный путь к файлику со сценарием/сценариями
Указываем, какой именно сценарий нужно прогнать
Через ключ
–serverвыбираем, какое окружение будем использовать
Тут надо помнить, что приличные люди указывают адреса окружений в OAS, но я не из их числа)–verbose— в исследовательских целях нас интересуют полный вывод в консоль и по запросам, и по ответамМожно также скормить креды для авторизации, но у меня они прямо в файле-сценарии
После нажатия Enter в консоли появится что-то подобное:
Running workflow simple_example.yml / example ✓ POST /cbt/v1/transfer/convert - step call-convert Request URL: https://your-service-host/cbt/v1/transfer/convert Request Headers: content-type: application/json accept: application/json authorization: Basic значение_токена Request Body: { "amount": 50000, "currencyFrom": "RUB", "currencyTo": "UZS", "direction": "TO_UZ" } Response status code: 200 Response time: 1061 ms Response Headers: cache-control: no-cache, no-store, max-age=0, must-revalidate connection: keep-alive content-length: 105 content-type: application/json date: Fri, 29 May 2026 12:55:33 GMT expires: 0 pragma: no-cache server: nginx vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers x-content-type-options: nosniff x-frame-options: DENY x-xss-protection: 0 Response Size: 105 bytes Response Body: { "amountFrom": 50000, "currencyFrom": "RUB", "amountTo": 7608974, "currencyTo": "UZS", "rate": "152.179490169555" } ✓ success criteria check - $statusCode == 200 || $statusCode == 400 || $statu... ✓ status code check - $statusCode in [200, 400, 401, 403] ✓ content-type check ✓ schema check Summary for simple_example.yml Workflows: 1 passed, 1 total Steps: 1 passed, 1 total Checks: 4 passed, 4 total Time: 2760ms ┌───────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├───────────────────────┼────────────┼─────────┼─────────┼──────────┤ │ ✓ simple_example.yml │ 1 │ 1 │ - │ - │ └───────────────────────┴────────────┴─────────┴─────────┴──────────┘
Самое интересное в выводе — в самом низу: видим, что выполнялся один шаг, он выполнился успешно и пройдены все 4 проверки:
код ответа входит в набор успешных, который мы описывали в файле
код ответа входит в набор описанных в файле спецификации метода
контент-тайп нужный
схема вернувшегося ответа совпадает с той, которая описана в спецификации
Скорее всего, именно последняя проверка дала название инструменту: можно проуважить сценарий об спеку и выявить несоответствия, например, в документации на портале.
В итоге имеем штуку, которая позволяет прогонять API-сценарий из некоторого числа методов и выводить подробности о том, что было сделано.
Вместо заключения
Может показаться, что в сочетании с redocli respect, Arazzo — отличный способ для автоматизации тестирования. Для простых API, где нет всяких хитростей типа подписи запросов и ответов, вполне.
Для чего-то сложнее — используйте привычные инструменты: авторы закладывали в Arazzo только упрощение процесса документирования API.
Как использовать ковры внутри команды?
Не знаю, честно. Я прекрасно понимаю, что сценарии в 7 методов, из которых 3 скорее информационные, не очень подходят для обкатки Arazzo в команде, которая это все реализовывала. Но для внешних партнёров, которые ваш сервис (и его спецификацию) видят впервые, файл-ковер позволит хотя бы понять, что и в каком порядке дергать.
И на этой мысли я закончу первую статью про Arazzo на русском языке)
PS: я не затрагивал MCP и прочие агентские шутки в контексте Arazzo. Существует мнение, что если раньше у вас был специальный сотрудник-человек для покупки авиабилетов, то сейчас у вас есть модный ИИ-агент для этого.
Чтобы помочь агенту c покупкой нужных билетов и можно использовать файл-ковер. И повесить гордую плашку “AI-ready” в документацию своего API.
Конец статьи и тот самый полный пример ковра с тремя сценариями и несколькими шагами в каждом (ссылка на скачивание).
Осторожно, много кода
arazzo: '1.0.1' info: title: Файл с тремя сценариями сразу version: '0.12.0' sourceDescriptions: - name: cbtApi type: openapi #url: ./oas.yaml # локальная спецификация рядом с тестом-кейсом url: https://%адрес_со_спекой%/redocusaurus/ru_crossborder.yaml workflows: - workflowId: credit summary: Линейный сценарий без ветвлений; externalTransferId берём из Date заголовка шага /convert x-security: - schemeName: basicAuth values: username: %тестовый логин% password: %тестовый пароль% steps: # 1) /convert (optional) - stepId: call-convert description: POST /convert (optional) operationId: cbtApi.convert requestBody: payload: amount: 1000 currencyFrom: USD currencyTo: UZS direction: TO_UZ successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 outputs: amountFrom: $response.body#/amountFrom currencyFrom: $response.body#/currencyFrom amountTo: $response.body#/amountTo currencyTo: $response.body#/currencyTo run_id: $response.header.Date # ← внешний id для следующих шагов onFailure: - name: continueToCardListEvenIfConvertFails type: goto stepId: card-list # 2) /card_list (optional) - stepId: card-list description: POST /card_list (by phone, optional) operationId: cbtApi.getReceiverCardsByPhone requestBody: payload: phone: "998900000101" successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 outputs: receiver_token: $response.body#/0/token #первый элемент массива onFailure: - name: continueToCheckCreditEvenIfCardListFails type: goto stepId: check-credit # 3) /check_credit - stepId: check-credit description: POST /check_credit (TOKEN из шага 2; externalTransferId на основе ответа шага 1) operationId: cbtApi.checkCredit requestBody: payload: externalTransferId: $steps.call-convert.outputs.run_id senderAmount: $steps.call-convert.outputs.amountFrom senderCurrencyCode: $steps.call-convert.outputs.currencyFrom receiverCurrencyCode: $steps.call-convert.outputs.currencyTo identificationType: TOKEN identificationValue: $steps.card-list.outputs.receiver_token senderCountry: KR sender: personFullName: "Leo Messi" successCriteria: - condition: $statusCode == 200 outputs: ext_id: $response.body#/externalTransferId onFailure: - name: continueToConfirmEvenIfCheckFails type: goto stepId: confirm-credit # 4) /confirm_credit - stepId: confirm-credit description: POST /confirm_credit (externalTransferId из шага 3) operationId: cbtApi.confirmCredit requestBody: payload: externalTransferId: $steps.check-credit.outputs.ext_id successCriteria: - condition: $statusCode == 200 onFailure: - name: continueToStatusEvenIfConfirmFails type: goto stepId: transfer-status # 5) /status (optional) - stepId: transfer-status description: POST /status (externalTransferId из шага 3) operationId: cbtApi.transferStatus requestBody: payload: externalTransferId: $steps.check-credit.outputs.ext_id successCriteria: - condition: $statusCode == 200 - workflowId: debit summary: Линейный сценарий без ветвлений; externalTransferId берём из Date заголовка шага /convert parameters: - name: Authorization in: header value: Basic %тестовые креды% steps: # 1) /convert (optional) - stepId: call-convert description: POST /convert (optional) operationId: cbtApi.convert requestBody: payload: amount: 1000000 currencyFrom: UZS currencyTo: USD direction: FROM_UZ successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 outputs: amountFrom: $response.body#/amountFrom currencyFrom: $response.body#/currencyFrom amountTo: $response.body#/amountTo currencyTo: $response.body#/currencyTo run_id: $response.header.Date # ← внешний id для следующих шагов onFailure: - name: continueToCardListEvenIfConvertFails type: goto stepId: card-list # 2) /check_debit - stepId: check_debit description: POST /check_debit (by token) operationId: cbtApi.registerDebit # при необходимости замените на ваш operationId requestBody: payload: externalTransferId: $steps.call-convert.outputs.run_id senderAmount: $steps.call-convert.outputs.amountFrom senderCurrencyCode: $steps.call-convert.outputs.currencyFrom receiverCurrencyCode: $steps.call-convert.outputs.currencyTo senderCardToken: vlKeomyUYGh8ktD+1hvI68kuApMKq1s8uyfgvHA= sender: personFirstName: Leo personLastName: Messi birthday: 1991-11-11 birthPlace: Macondo residencyCode: 1 document: identityDocumentCode: 4 identityDocumentSeries: 4013 identityDocumentNumber: 844659 identityDocumentIssuer: InterNational Passports identityDocumentIssueDate: 2011-12-12 pinfl: 30101800050014 receiver: personFullName: Donald Duck receiverAccount: 553609******2598 receiverCountry: KR phone: 998507583221 successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 outputs: ext_id: $response.body#/externalTransferId onFailure: - name: continueToConfirmEvenIfCheckFails type: goto stepId: confirm-debit # 3) /resend_otp - stepId: resend-otp description: POST /resend_otp (externalTransferId из шага 2) operationId: cbtApi.resendOTP requestBody: payload: externalTransferId: $steps.check_debit.outputs.ext_id successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 onFailure: - name: continueToStatusEvenIfConfirmFails type: goto stepId: transfer-status # 4) /confirm_debit (optional) - stepId: confirm-debit description: POST /confirm_debit (externalTransferId из шага 2) operationId: cbtApi.confirmDebit requestBody: payload: externalTransferId: $steps.check_debit.outputs.ext_id code: 112233 successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 onFailure: - name: continueToStatusEvenIfConfirmFails type: goto stepId: transfer-status # 5) /status (optional) - stepId: transfer-status description: POST /status (externalTransferId из шага 2) operationId: cbtApi.transferStatus requestBody: payload: externalTransferId: $steps.check_debit.outputs.ext_id successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 # 6) /cancel_debit - stepId: cancel-debit description: POST /cancel_debit (externalTransferId из шага 2) operationId: cbtApi.cancelDebit requestBody: payload: externalTransferId: $steps.check_debit.outputs.ext_id successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 # 7) /status (optional) - stepId: re-transfer-status description: POST /status (externalTransferId из шага 2) operationId: cbtApi.transferStatus requestBody: payload: externalTransferId: $steps.check_debit.outputs.ext_id successCriteria: - condition: $statusCode == 200 || $statusCode == 400 || $statusCode == 401 || $statusCode == 403 - workflowId: accInfo summary: Последовательный вызов /transfer_list, /closing_balance, /account/operations parameters: - name: Authorization in: header value: Basic %тестовые креды% steps: - stepId: transfer-list description: POST /transfer_list operationId: cbtApi.transferList requestBody: payload: createTime: from: "2026-02-27T09:55:30.250" to: "2026-03-27T09:55:30.250" page: 1 limit: 25 successCriteria: - context: $statusCode type: regex condition: "^(200|400|401|403)$" - stepId: closing-balance description: GET /account/closing_balance operationId: cbtApi.getClosingBalance parameters: - name: accountNumber in: query value: "29126840100001190019" successCriteria: - context: $statusCode type: regex condition: "^(200|400|401|500|504)$" - stepId: account-operations description: GET /account/operations operationId: cbtApi.getAccountOperations parameters: - name: accountNumber in: query value: "29126840100001190019" - name: startDate in: query value: "2026-01-20" - name: endDate in: query value: "2026-03-21" successCriteria: - context: $statusCode type: regex condition: "^(200|400|401|500|504)$"
