Всем привет!
Я Антон, системный аналитик из команды трансграничных переводов в Uzum Fintech, и от меня уже два месяца ждут текст по результатам выступления на внутреннем митапе.
Поэтому сегодня мне придется рассказать про то, как с помощью свежего стандарта от OpenApi Initiative можно упростить процесс понимания разных чужих апишек и быстрее интегрироваться с какой-нибудь чужой вундервафлей.
Проблемы с описанием API
Дисклеймер
Усложнение API
ДокументацияКовры
Простейший пример
ИнструментыВместо заключения
Проблемы с описанием 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)$"

























