Всем привет!

Я Антон, системный аналитик из команды трансграничных переводов в Uzum Fintech, и от меня уже два месяца ждут текст по результатам выступления на внутреннем митапе.

Поэтому сегодня мне придется рассказать про то, как с помощью свежего стандарта от OpenApi Initiative можно упростить процесс понимания разных чужих апишек и быстрее интегрироваться с какой-нибудь чужой вундервафлей.

  1. Проблемы с описанием API

  2. Дисклеймер

  3. Усложнение API

  4. Документация

  5. Ковры

  6. Простейший пример

  7. Инструменты

  8. Вместо заключения

Проблемы с описанием API

Дисклеймер

В данной статье под «интеграцией» будет пониматься скорее какая-то совсем внешняя история. Ребята из соседней команды, которые помогут и всё объяснят с расстояния пары сообщений в телеграме/slack’e тут не очень подходят, но полезно будет и для взаимодействия с ними.
А еще будет полезно разработчикам, тестировщикам, аналитикам, продактам, техписам и даже биздевам — в общем всем, кто так или иначе трогает API.

Усложнение API

Начнем с того, что сейчас очень редко можно встретить интеграцию, в рамках которой достаточно вызвать всего один метод, чтобы получить желаемый бизнес-результат. Чаще приходится дергать в нужной последовательности несколько разных эндпоинтов, причем последовательность может меняться в зависимости от полученного промежуточного результата. То есть происходит переход от «вызова метода» к сценариям. 

И тогда для успешной интеграции надо:

  1. Знать, что нужно получить на выходе — бизнесовый результат

  2. Знать сценарий, как достичь этого результата

  3. Знать данные, которые нужны для всего сценария в целом и для каждого шага в частности

Ладно, если бы это было в рамках RESTful API, где глаголы методов и названия ресурсов как-то подсказывают, что делать, но существует классный тренд на смесь REST- и RPC-подходов, где, к сожалению, нет устоявшихся и общепринятых норм: команды придерживаются какой-то одной концепции только в рамках одного конкретного сервиса.

Или, если рассмотреть абстрактный кровавый энтерпрайз, может возникнуть что-то очень похожее на нулевой уровень зрелости REST API по Ричардсону, где все операции делаются через один-два универсальных POST-эндпоинта, но выполняемое действие зависит от передаваемой в теле запроса «команды» и данных. Команд при этом может быть много, и все они требуют абсолютно разных наборов данных.

Во всё это легко вникнуть, если читатель документации — сам автор, в других же случаях явно будут вопросы разной степени глубины и отчаяния. 

Усложнение самих API — это только половина беды.

Документация

В отношении документации тоже не все так хорошо, как хотелось бы видеть в 2к26. В лучшем случае она вообще существует. А дальше начинаются всякие разные ухищрения или даже извращения.

То с чем приходилось сталкиваться на практике (без имен):

  1. Всеобъемлющий портал с документацией на все эндпоинты, с описанием структур данных, enum’ов, схем и кучей текста.
    Это очень тяжело поддерживать в актуальном состоянии и в любом случае остаются вопросы, как работает та или иная штука в таком-то (именно вашем) кейсе, которые приходится решать через поддержку. Но это скорее около-госовая история и то, к чему надо стремиться

  2. Документация на весь сервис в виде .PDF или .docx.
    Тут всё по классике: листинги запросов и ответов даны в две колонки для экономии места, но всё равно не влезают на одну страницу. Сразу возрастает сложность при копировании нужного куска JSON’a 

  3. Коллекция запросов для Postman’a, к которой нет сопроводительного текста, где описана структура папок и что же в конечном счете делает или должен делать каждый из запросов. Или этот текст есть, но вам его не прислали. Или прислали, но через 2 года 

И два вспомогательных артефакта:

  1. Sequence-диаграмма на пару экранов по высоте и ширине, любимая большинством аналитиков, и которая не предназначена для хранения всей необходимой информации (типа тел запросов)

  2. 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)$"